jfs-components 0.0.79 → 0.0.85

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 (138) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/commonjs/components/AppBar/AppBar.js +70 -6
  3. package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
  4. package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
  5. package/lib/commonjs/components/Attached/Attached.js +76 -7
  6. package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
  7. package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
  8. package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
  9. package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
  10. package/lib/commonjs/components/Drawer/Drawer.js +6 -1
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
  12. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  13. package/lib/commonjs/components/FormField/FormField.js +1 -14
  14. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
  15. package/lib/commonjs/components/ListItem/ListItem.js +6 -11
  16. package/lib/commonjs/components/MessageField/MessageField.js +1 -13
  17. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
  18. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
  19. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
  20. package/lib/commonjs/components/Spinner/Spinner.js +217 -0
  21. package/lib/commonjs/components/TextInput/TextInput.js +33 -18
  22. package/lib/commonjs/components/index.js +34 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
  25. package/lib/commonjs/icons/components/IconArrowup.js +19 -0
  26. package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
  27. package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
  28. package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
  29. package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
  30. package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
  31. package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
  32. package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
  33. package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
  34. package/lib/commonjs/icons/components/IconSignin.js +19 -0
  35. package/lib/commonjs/icons/components/IconSignout.js +19 -0
  36. package/lib/commonjs/icons/components/index.js +132 -0
  37. package/lib/commonjs/icons/registry.js +2 -2
  38. package/lib/module/components/AppBar/AppBar.js +70 -6
  39. package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
  40. package/lib/module/components/AreaLineChart/chartMath.js +242 -0
  41. package/lib/module/components/Attached/Attached.js +76 -7
  42. package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
  43. package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
  44. package/lib/module/components/Checkbox/Checkbox.js +18 -2
  45. package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
  46. package/lib/module/components/Drawer/Drawer.js +6 -1
  47. package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
  48. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  49. package/lib/module/components/FormField/FormField.js +3 -16
  50. package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
  51. package/lib/module/components/ListItem/ListItem.js +6 -11
  52. package/lib/module/components/MessageField/MessageField.js +3 -15
  53. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
  54. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
  55. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
  56. package/lib/module/components/Spinner/Spinner.js +212 -0
  57. package/lib/module/components/TextInput/TextInput.js +34 -19
  58. package/lib/module/components/index.js +4 -0
  59. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  60. package/lib/module/icons/components/IconArrowdown.js +12 -0
  61. package/lib/module/icons/components/IconArrowup.js +12 -0
  62. package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
  63. package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
  64. package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
  65. package/lib/module/icons/components/IconChevronupcircle.js +12 -0
  66. package/lib/module/icons/components/IconOsnavback.js +12 -0
  67. package/lib/module/icons/components/IconOsnavcenter.js +12 -0
  68. package/lib/module/icons/components/IconOsnavhome.js +12 -0
  69. package/lib/module/icons/components/IconOsnavtask.js +12 -0
  70. package/lib/module/icons/components/IconSignin.js +12 -0
  71. package/lib/module/icons/components/IconSignout.js +12 -0
  72. package/lib/module/icons/components/index.js +12 -0
  73. package/lib/module/icons/registry.js +2 -2
  74. package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
  75. package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
  76. package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
  77. package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
  78. package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
  79. package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
  80. package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
  81. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
  82. package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
  83. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
  84. package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
  85. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
  86. package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
  87. package/lib/typescript/src/components/index.d.ts +4 -0
  88. package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
  89. package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
  90. package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
  91. package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
  92. package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
  93. package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
  94. package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
  95. package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
  96. package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
  97. package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
  98. package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
  99. package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
  100. package/lib/typescript/src/icons/components/index.d.ts +12 -0
  101. package/lib/typescript/src/icons/registry.d.ts +1 -1
  102. package/package.json +3 -2
  103. package/src/components/AppBar/AppBar.tsx +92 -12
  104. package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
  105. package/src/components/AreaLineChart/chartMath.ts +265 -0
  106. package/src/components/Attached/Attached.tsx +94 -7
  107. package/src/components/BubbleChart/BubbleChart.tsx +319 -0
  108. package/src/components/BubbleChart/bubblePacking.ts +397 -0
  109. package/src/components/Checkbox/Checkbox.tsx +14 -2
  110. package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
  111. package/src/components/Drawer/Drawer.tsx +4 -0
  112. package/src/components/DropdownInput/DropdownInput.tsx +54 -20
  113. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
  114. package/src/components/FormField/FormField.tsx +3 -19
  115. package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
  116. package/src/components/ListItem/ListItem.tsx +14 -16
  117. package/src/components/MessageField/MessageField.tsx +3 -18
  118. package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
  119. package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
  120. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
  121. package/src/components/Spinner/Spinner.tsx +273 -0
  122. package/src/components/TextInput/TextInput.tsx +37 -19
  123. package/src/components/index.ts +4 -0
  124. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  125. package/src/icons/components/IconArrowdown.tsx +11 -0
  126. package/src/icons/components/IconArrowup.tsx +11 -0
  127. package/src/icons/components/IconChevrondowncircle.tsx +11 -0
  128. package/src/icons/components/IconChevronleftcircle.tsx +11 -0
  129. package/src/icons/components/IconChevronrightcircle.tsx +11 -0
  130. package/src/icons/components/IconChevronupcircle.tsx +11 -0
  131. package/src/icons/components/IconOsnavback.tsx +11 -0
  132. package/src/icons/components/IconOsnavcenter.tsx +11 -0
  133. package/src/icons/components/IconOsnavhome.tsx +11 -0
  134. package/src/icons/components/IconOsnavtask.tsx +11 -0
  135. package/src/icons/components/IconSignin.tsx +11 -0
  136. package/src/icons/components/IconSignout.tsx +11 -0
  137. package/src/icons/components/index.ts +12 -0
  138. package/src/icons/registry.ts +49 -1
@@ -11,7 +11,6 @@ import {
11
11
  type PressableStateCallbackType,
12
12
  } from 'react-native'
13
13
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
14
- import IconCapsule from '../IconCapsule/IconCapsule'
15
14
  import NavArrow from '../NavArrow/NavArrow'
16
15
  import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
17
16
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
@@ -21,8 +20,8 @@ type ListItemProps = {
21
20
  title?: string;
22
21
  supportText?: string;
23
22
  showSupportText?: boolean;
24
- /** Leading slot (Figma "leading"). Defaults to an `IconCapsule` when omitted. */
25
- leading?: React.ReactNode;
23
+ /** Leading slot (Figma "leading"). Omitted or `null` renders nothing. */
24
+ leading?: React.ReactNode | null;
26
25
  supportSlot?: React.ReactNode;
27
26
  /** Trailing slot (Figma "trailing"), e.g. `MoneyValue` or `Button`. Horizontal layout only. */
28
27
  trailing?: React.ReactNode;
@@ -188,7 +187,7 @@ const verticalSupportTextOverride: TextStyle = { textAlign: 'center' }
188
187
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
189
188
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
190
189
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
191
- * @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
190
+ * @param {React.ReactNode|null} [props.leading] - Optional leading slot. Omitted or `null` renders nothing.
192
191
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
193
192
  * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
194
193
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
@@ -263,16 +262,15 @@ function ListItemImpl({
263
262
  // Process leading slot to pass modes to children. Memoized on
264
263
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
265
264
  const leadingElement = useMemo(() => {
266
- const processed = leading
267
- ? cloneChildrenWithModes(
268
- React.Children.toArray(leading),
269
- tokens.resolvedModes,
270
- SLOT_FORCED_MODES
271
- )
272
- : []
273
- if (processed.length === 0) {
274
- return <IconCapsule modes={tokens.resolvedModes} accessibilityLabel={undefined} />
275
- }
265
+ if (leading == null) return null
266
+
267
+ const processed = cloneChildrenWithModes(
268
+ React.Children.toArray(leading),
269
+ tokens.resolvedModes,
270
+ SLOT_FORCED_MODES
271
+ )
272
+ if (processed.length === 0) return null
273
+
276
274
  return processed.length === 1 ? processed[0] : processed
277
275
  }, [leading, tokens.resolvedModes])
278
276
 
@@ -373,7 +371,7 @@ function ListItemImpl({
373
371
  if (layout === 'Horizontal') {
374
372
  const innerContent = (
375
373
  <View style={innerContentStyleArray}>
376
- {leadingElement}
374
+ {leadingElement ?? null}
377
375
  <View
378
376
  style={{
379
377
  flex: 1,
@@ -431,7 +429,7 @@ function ListItemImpl({
431
429
  // Vertical layout — icon on top, support text/slot below
432
430
  const verticalContent = (
433
431
  <View style={verticalContentStyleArray}>
434
- {leadingElement}
432
+ {leadingElement ?? null}
435
433
  {renderSupportContent()}
436
434
  </View>
437
435
  )
@@ -1,8 +1,7 @@
1
- import React, { useCallback, useMemo, useRef, useState } from 'react'
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
5
- Pressable,
6
5
  TextInput as RNTextInput,
7
6
  type StyleProp,
8
7
  type TextInputProps as RNTextInputProps,
@@ -297,15 +296,6 @@ function MessageField({
297
296
  const [isFocused, setIsFocused] = useState(false)
298
297
  const interactive = !isDisabled && !isReadOnly
299
298
 
300
- // Ref to the native textarea so tapping anywhere in the (padded) textarea
301
- // container focuses it on the FIRST tap, fixing the Android "two taps to
302
- // open the keyboard" issue.
303
- const inputRef = useRef<RNTextInput>(null)
304
- const focusInput = useCallback(() => {
305
- if (!interactive) return
306
- inputRef.current?.focus()
307
- }, [interactive])
308
-
309
299
  const { modes: globalModes } = useTokens()
310
300
  const baseModes = useMemo(
311
301
  () => ({ ...globalModes, ...propModes }),
@@ -508,13 +498,8 @@ function MessageField({
508
498
  </View>
509
499
  )}
510
500
 
511
- <Pressable
512
- style={[textareaContainerStyle, textareaStyle]}
513
- onPress={focusInput}
514
- accessible={false}
515
- >
501
+ <View style={[textareaContainerStyle, textareaStyle]}>
516
502
  <RNTextInput
517
- ref={inputRef}
518
503
  multiline
519
504
  value={currentValue}
520
505
  onChangeText={handleChangeText}
@@ -529,7 +514,7 @@ function MessageField({
529
514
  accessibilityHint={accessibilityHint}
530
515
  style={[inputTextStyle, inputStyle]}
531
516
  />
532
- </Pressable>
517
+ </View>
533
518
 
534
519
  {shouldShowCounter && (
535
520
  <Text style={counterTextStyle} accessibilityElementsHidden>
@@ -22,6 +22,12 @@ export type MetricLegendItemProps = {
22
22
  * `metricLegendItem/indicator/bg` design token.
23
23
  */
24
24
  indicatorColor?: string
25
+ /**
26
+ * Shape of the leading indicator. `'dot'` (default) renders the small
27
+ * circle used in categorical legends; `'line'` renders a short
28
+ * horizontal bar, matching the legend of a line chart.
29
+ */
30
+ indicatorShape?: 'dot' | 'line'
25
31
  /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
26
32
  modes?: Record<string, any>
27
33
  /** Override container styles. */
@@ -46,6 +52,7 @@ function MetricLegendItem({
46
52
  label = 'Current (4 months)',
47
53
  value,
48
54
  indicatorColor,
55
+ indicatorShape = 'dot',
49
56
  modes = EMPTY_MODES,
50
57
  style,
51
58
  indicatorStyle,
@@ -107,12 +114,19 @@ function MetricLegendItem({
107
114
  >
108
115
  <View
109
116
  style={[
110
- {
111
- width: indicatorSize,
112
- height: indicatorSize,
113
- borderRadius: indicatorRadius,
114
- backgroundColor: indicatorBg,
115
- },
117
+ indicatorShape === 'line'
118
+ ? {
119
+ width: indicatorSize * 2,
120
+ height: Math.max(2, Math.round(indicatorSize / 4)),
121
+ borderRadius: indicatorRadius,
122
+ backgroundColor: indicatorBg,
123
+ }
124
+ : {
125
+ width: indicatorSize,
126
+ height: indicatorSize,
127
+ borderRadius: indicatorRadius,
128
+ backgroundColor: indicatorBg,
129
+ },
116
130
  indicatorStyle,
117
131
  ]}
118
132
  />
@@ -1,9 +1,9 @@
1
- import React, { type ReactNode, isValidElement, cloneElement } from 'react'
1
+ import React, { type ReactNode } from 'react'
2
2
  import { View, Text, type ViewStyle, type TextStyle } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
5
  import IconCapsule from '../IconCapsule/IconCapsule'
6
- import { EMPTY_MODES } from '../../utils/react-utils'
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
7
7
 
8
8
  export type PaymentFeedbackProps = {
9
9
  /** Large heading text, typically a monetary value (e.g. "₹50,000") */
@@ -20,7 +20,11 @@ export type PaymentFeedbackProps = {
20
20
  iconName?: string
21
21
  /** Optional custom media slot that replaces the default IconCapsule */
22
22
  renderMedia?: ReactNode
23
- /** Mode configuration for design tokens */
23
+ /**
24
+ * Mode configuration for design tokens. Also drives the default
25
+ * IconCapsule's color — pass `AppearanceSystem: 'positive' | 'warning' |
26
+ * 'negative'` to render a green/orange/red capsule (defaults to `positive`).
27
+ */
24
28
  modes?: Record<string, any>
25
29
  style?: ViewStyle
26
30
  }
@@ -28,7 +32,7 @@ export type PaymentFeedbackProps = {
28
32
  export default function PaymentFeedback({
29
33
  title = '₹50,000',
30
34
  subtitle = 'Payment successful',
31
- body = 'Your payment has been\nsuccessfully processed.',
35
+ body,
32
36
  details = '18 March 2025, 4:15 pm\nTransaction ID: TXN121466784',
33
37
  showDetails = true,
34
38
  iconName = 'ic_confirm',
@@ -123,14 +127,17 @@ export default function PaymentFeedback({
123
127
  textAlign: 'center',
124
128
  }
125
129
 
126
- const mediaContent = isValidElement(renderMedia)
127
- ? cloneElement(renderMedia as React.ReactElement<any>, { modes })
128
- : renderMedia
130
+ // Cascade modes into a custom media slot (per the modes-cascade convention);
131
+ // any modes the consumer set on the slot child still take precedence.
132
+ const mediaContent =
133
+ renderMedia != null ? cloneChildrenWithModes(renderMedia, modes) : null
129
134
 
130
135
  const defaultMedia = (
131
136
  <IconCapsule
132
137
  iconName={iconName}
133
- modes={{ ...modes, 'Icon Capsule Size': 'L', Emphasis: 'High', 'Semantic Intent': 'System', AppearanceSystem: 'positive' }}
138
+ // `positive` is the default; consumers override the capsule color by
139
+ // passing `AppearanceSystem` (or any other mode) via the `modes` prop.
140
+ modes={{ AppearanceSystem: 'positive', ...modes, 'Icon Capsule Size': 'L', Emphasis: 'High', 'Semantic Intent': 'System' }}
134
141
  />
135
142
  )
136
143
 
@@ -1,4 +1,4 @@
1
- import React, { useState, useCallback } from 'react';
1
+ import React from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -7,12 +7,15 @@ import {
7
7
  type StyleProp,
8
8
  type ViewStyle,
9
9
  type TextStyle,
10
- type LayoutChangeEvent,
11
10
  } from 'react-native';
12
11
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
13
12
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
14
13
  import Icon from '../../icons/Icon';
15
14
 
15
+ /** Figma grid: label column 1.8fr, each plan column 1fr. */
16
+ const LABEL_COLUMN_FR = 1.8;
17
+ const PLAN_COLUMN_FR = 1;
18
+
16
19
  /**
17
20
  * A single plan column header (the label column has no header of its own).
18
21
  */
@@ -69,13 +72,6 @@ export type PlanComparisonCardProps = {
69
72
  columns?: PlanComparisonColumn[];
70
73
  /** Feature rows compared across the plan columns. */
71
74
  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
75
  /** Design token modes for theming (e.g. `{ "Color Mode": "Light" }`). */
80
76
  modes?: Record<string, any>;
81
77
  /** Override the outer container style. */
@@ -93,92 +89,40 @@ const DEFAULT_ROWS: PlanComparisonRow[] = [
93
89
  { label: 'Bonus JioGold', showInfo: true, values: [false, '1%'] },
94
90
  ];
95
91
 
92
+ /** Keeps every text layer on a single line. */
93
+ const NO_WRAP_TEXT: TextStyle = {
94
+ flexShrink: 0,
95
+ ...(Platform.OS === 'web' ? { whiteSpace: 'nowrap' as const } : {}),
96
+ };
97
+
98
+ const labelColumnStyle: ViewStyle = {
99
+ flex: LABEL_COLUMN_FR,
100
+ minWidth: 0,
101
+ };
102
+
103
+ const planColumnStyle: ViewStyle = {
104
+ flex: PLAN_COLUMN_FR,
105
+ minWidth: 0,
106
+ alignItems: 'center',
107
+ };
108
+
96
109
  /**
97
110
  * PlanComparisonCard renders a compact comparison table that pits the user's
98
111
  * current plan against one or more alternative plans across a set of feature
99
112
  * rows. Implementation of Figma node `4498:2968` (`PlanComparisonCard`).
100
113
  *
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.
114
+ * Columns use a 1.8fr / 1fr flex ratio (label vs plan), matching the Figma grid.
104
115
  *
105
116
  * @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
  */
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
118
  function PlanComparisonCard({
124
119
  columns = DEFAULT_COLUMNS,
125
120
  rows = DEFAULT_ROWS,
126
- labelColumnFlex = 0,
127
121
  modes = EMPTY_MODES,
128
122
  style,
129
123
  }: 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
124
  const gap = (getVariableByName('planComparisonCard/gap', modes) as number) ?? 16;
180
125
 
181
- // Header
182
126
  const headerFg = (getVariableByName('planComparisonCard/header/fg', modes) as string) ?? '#ffffff';
183
127
  const headerBrandFg = (getVariableByName('planComparisonCard/header/brand/fg', modes) as string) ?? '#cea15a';
184
128
  const headerFontSize = (getVariableByName('planComparisonCard/header/fontSize', modes) as number) ?? 14;
@@ -186,20 +130,17 @@ function PlanComparisonCard({
186
130
  const headerLineHeight = (getVariableByName('planComparisonCard/header/lineHeight', modes) as number) ?? 18;
187
131
  const headerFontWeight = (getVariableByName('planComparisonCard/header/fontWeight', modes) as number | string) ?? '500';
188
132
 
189
- // Table
190
133
  const tableBackground = (getVariableByName('planComparisonCard/tableRow/background', modes) as string) ?? '#141414';
191
134
  const tableRadius = (getVariableByName('planComparisonCard/tableRow/radius', modes) as number) ?? 16;
192
135
  const tableBorderSize = (getVariableByName('planComparisonCard/tableRow/border/size', modes) as number) ?? 1;
193
136
  const tableBorderColor = (getVariableByName('planComparisonCard/tableRow/border/color', modes) as string) ?? '#1e1a14';
194
137
 
195
- // Cell
196
138
  const cellPadding = (getVariableByName('planComparisonCard/tableCell/padding', modes) as number) ?? 12;
197
139
  const cellGap = (getVariableByName('planComparisonCard/tableCell/gap', modes) as number) ?? 2;
198
140
  const cellMinHeight = (getVariableByName('planComparisonCard/tableCell/height', modes) as number) ?? 46;
199
141
  const cellBorderSize = (getVariableByName('planComparisonCard/tableCell/border/size', modes) as number) ?? 1;
200
142
  const cellBorderColor = (getVariableByName('planComparisonCard/tableCell/border/color', modes) as string) ?? '#1e1a14';
201
143
 
202
- // Cell label
203
144
  const labelColor = (getVariableByName('planComparisonCard/tableCell/label/color', modes) as string) ?? '#ffffff';
204
145
  const labelDisabledColor = (getVariableByName('planComparisonCard/tableCell/label/disabled/color', modes) as string) ?? '#91949c';
205
146
  const labelFontSize = (getVariableByName('planComparisonCard/tableCell/label/fontSize', modes) as number) ?? 12;
@@ -207,14 +148,12 @@ function PlanComparisonCard({
207
148
  const labelLineHeight = (getVariableByName('planComparisonCard/tableCell/label/lineHeight', modes) as number) ?? 16;
208
149
  const labelFontWeight = (getVariableByName('planComparisonCard/tableCell/label/fontWeight', modes) as number | string) ?? '400';
209
150
 
210
- // Cell value
211
151
  const valueColor = (getVariableByName('planComparisonCard/tableCell/value/color', modes) as string) ?? '#ffffff';
212
152
  const valueFontSize = (getVariableByName('planComparisonCard/tableCell/value/fontSize', modes) as number) ?? 12;
213
153
  const valueFontFamily = (getVariableByName('planComparisonCard/tableCell/value/fontFamily', modes) as string) ?? 'JioType Var';
214
154
  const valueLineHeight = (getVariableByName('planComparisonCard/tableCell/value/lineHeight', modes) as number) ?? 16;
215
155
  const valueFontWeight = (getVariableByName('planComparisonCard/tableCell/value/fontWeight', modes) as number | string) ?? '500';
216
156
 
217
- // Icon
218
157
  const iconColor = (getVariableByName('planComparisonCard/icon/color', modes) as string) ?? '#ffffff';
219
158
  const iconSize = (getVariableByName('planComparisonCard/icon/size', modes) as number) ?? 16;
220
159
 
@@ -248,13 +187,29 @@ function PlanComparisonCard({
248
187
  textAlign: 'center',
249
188
  };
250
189
 
251
- const planHeaderColumnStyle: ViewStyle = {
190
+ const rowStyle: ViewStyle = {
191
+ flexDirection: 'row',
192
+ width: '100%',
193
+ };
194
+
195
+ const labelCellStyle: ViewStyle = {
196
+ flexDirection: 'row',
197
+ alignItems: 'center',
198
+ gap: cellGap,
199
+ padding: cellPadding,
200
+ minHeight: cellMinHeight,
201
+ };
202
+
203
+ const valueCellStyle: ViewStyle = {
204
+ flexDirection: 'row',
252
205
  alignItems: 'center',
253
206
  justifyContent: 'center',
207
+ padding: cellPadding,
208
+ minHeight: cellMinHeight,
209
+ width: '100%',
254
210
  };
255
211
 
256
212
  const renderValue = (value: PlanComparisonCellValue, cellKey: React.Key) => {
257
- // "Not available" → muted cross icon.
258
213
  if (value === false) {
259
214
  return (
260
215
  <Icon
@@ -265,11 +220,9 @@ function PlanComparisonCard({
265
220
  />
266
221
  );
267
222
  }
268
- // Empty cell.
269
223
  if (value === null || value === undefined || value === true) {
270
224
  return null;
271
225
  }
272
- // Text content.
273
226
  if (typeof value === 'string' || typeof value === 'number') {
274
227
  return (
275
228
  <Text key={cellKey} style={valueTextStyle}>
@@ -277,67 +230,32 @@ function PlanComparisonCard({
277
230
  </Text>
278
231
  );
279
232
  }
280
- // Custom node — forward modes so themed children stay in sync.
281
233
  return cloneChildrenWithModes(value, modes);
282
234
  };
283
235
 
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
236
  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]}
237
+ <View style={[{ gap, width: '100%' }, style]}>
238
+ {/* Header row — same 1.8fr / 1fr grid as the table */}
239
+ <View style={rowStyle}>
240
+ <View style={labelColumnStyle} />
241
+ {columns.map((column, index) => (
242
+ <View key={column.label ?? index} style={planColumnStyle}>
243
+ <Text
244
+ style={[
245
+ headerTextStyle,
246
+ { color: column.brand ? headerBrandFg : headerFg },
247
+ ]}
319
248
  >
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
- })}
249
+ {column.label}
250
+ </Text>
251
+ </View>
252
+ ))}
334
253
  </View>
335
254
 
336
- {/* Single rounded table — columns size to their widest cell */}
255
+ {/* Table body */}
337
256
  <View
338
257
  style={{
339
- flexDirection: 'row',
340
- alignSelf: 'flex-start',
258
+ width: '100%',
341
259
  backgroundColor: tableBackground,
342
260
  borderWidth: tableBorderSize,
343
261
  borderColor: tableBorderColor,
@@ -345,27 +263,21 @@ function PlanComparisonCard({
345
263
  overflow: 'hidden',
346
264
  }}
347
265
  >
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
- >
266
+ {rows.map((row, rowIndex) => {
267
+ const isLast = rowIndex === rows.length - 1;
268
+ const showInfo = row.showInfo || row.onInfoPress != null;
269
+ return (
270
+ <View
271
+ key={row.key ?? `${row.label}-${rowIndex}`}
272
+ style={[
273
+ rowStyle,
274
+ {
275
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
276
+ borderBottomColor: cellBorderColor,
277
+ },
278
+ ]}
279
+ >
280
+ <View style={[labelColumnStyle, labelCellStyle]}>
369
281
  <Text style={labelTextStyle}>{row.label}</Text>
370
282
  {showInfo &&
371
283
  (row.onInfoPress ? (
@@ -381,41 +293,19 @@ function PlanComparisonCard({
381
293
  <Icon name="ic_info" size={iconSize} color={iconColor} />
382
294
  ))}
383
295
  </View>
384
- );
385
- })}
386
- </View>
387
296
 
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 (
297
+ {columns.map((column, colIndex) => (
399
298
  <View
400
- key={row.key ?? `${row.label}-${rowIndex}`}
401
- style={[
402
- valueCellStyle,
403
- {
404
- borderBottomWidth: isLast ? 0 : cellBorderSize,
405
- borderBottomColor: cellBorderColor,
406
- },
407
- ]}
299
+ key={column.label ?? colIndex}
300
+ style={[planColumnStyle, valueCellStyle]}
408
301
  >
409
- <View style={{ flexShrink: 0 }}>
410
- {renderValue(
411
- row.values?.[colIndex],
412
- `${rowIndex}-${colIndex}`,
413
- )}
414
- </View>
302
+ {renderValue(
303
+ row.values?.[colIndex],
304
+ `${rowIndex}-${colIndex}`,
305
+ )}
415
306
  </View>
416
- );
417
- })}
418
- </View>
307
+ ))}
308
+ </View>
419
309
  );
420
310
  })}
421
311
  </View>