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.
- package/CHANGELOG.md +29 -0
- package/lib/commonjs/components/AppBar/AppBar.js +70 -6
- package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
- package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
- package/lib/commonjs/components/Attached/Attached.js +76 -7
- package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
- package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
- package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
- package/lib/commonjs/components/Drawer/Drawer.js +6 -1
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/commonjs/components/FormField/FormField.js +1 -14
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/commonjs/components/ListItem/ListItem.js +6 -11
- package/lib/commonjs/components/MessageField/MessageField.js +1 -13
- package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
- package/lib/commonjs/components/Spinner/Spinner.js +217 -0
- package/lib/commonjs/components/TextInput/TextInput.js +33 -18
- package/lib/commonjs/components/index.js +34 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
- package/lib/commonjs/icons/components/IconArrowup.js +19 -0
- package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
- package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
- package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
- package/lib/commonjs/icons/components/IconSignin.js +19 -0
- package/lib/commonjs/icons/components/IconSignout.js +19 -0
- package/lib/commonjs/icons/components/index.js +132 -0
- package/lib/commonjs/icons/registry.js +2 -2
- package/lib/module/components/AppBar/AppBar.js +70 -6
- package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
- package/lib/module/components/AreaLineChart/chartMath.js +242 -0
- package/lib/module/components/Attached/Attached.js +76 -7
- package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
- package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
- package/lib/module/components/Checkbox/Checkbox.js +18 -2
- package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
- package/lib/module/components/Drawer/Drawer.js +6 -1
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
- package/lib/module/components/FormField/FormField.js +3 -16
- package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
- package/lib/module/components/ListItem/ListItem.js +6 -11
- package/lib/module/components/MessageField/MessageField.js +3 -15
- package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
- package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
- package/lib/module/components/Spinner/Spinner.js +212 -0
- package/lib/module/components/TextInput/TextInput.js +34 -19
- package/lib/module/components/index.js +4 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/components/IconArrowdown.js +12 -0
- package/lib/module/icons/components/IconArrowup.js +12 -0
- package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
- package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
- package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
- package/lib/module/icons/components/IconChevronupcircle.js +12 -0
- package/lib/module/icons/components/IconOsnavback.js +12 -0
- package/lib/module/icons/components/IconOsnavcenter.js +12 -0
- package/lib/module/icons/components/IconOsnavhome.js +12 -0
- package/lib/module/icons/components/IconOsnavtask.js +12 -0
- package/lib/module/icons/components/IconSignin.js +12 -0
- package/lib/module/icons/components/IconSignout.js +12 -0
- package/lib/module/icons/components/index.js +12 -0
- package/lib/module/icons/registry.js +2 -2
- package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
- package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
- package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
- package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
- package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
- package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
- package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
- package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
- package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
- package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
- package/lib/typescript/src/components/index.d.ts +4 -0
- package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
- package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
- package/lib/typescript/src/icons/components/index.d.ts +12 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +3 -2
- package/src/components/AppBar/AppBar.tsx +92 -12
- package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
- package/src/components/AreaLineChart/chartMath.ts +265 -0
- package/src/components/Attached/Attached.tsx +94 -7
- package/src/components/BubbleChart/BubbleChart.tsx +319 -0
- package/src/components/BubbleChart/bubblePacking.ts +397 -0
- package/src/components/Checkbox/Checkbox.tsx +14 -2
- package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
- package/src/components/Drawer/Drawer.tsx +4 -0
- package/src/components/DropdownInput/DropdownInput.tsx +54 -20
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
- package/src/components/FormField/FormField.tsx +3 -19
- package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
- package/src/components/ListItem/ListItem.tsx +14 -16
- package/src/components/MessageField/MessageField.tsx +3 -18
- package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
- package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
- package/src/components/Spinner/Spinner.tsx +273 -0
- package/src/components/TextInput/TextInput.tsx +37 -19
- package/src/components/index.ts +4 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/components/IconArrowdown.tsx +11 -0
- package/src/icons/components/IconArrowup.tsx +11 -0
- package/src/icons/components/IconChevrondowncircle.tsx +11 -0
- package/src/icons/components/IconChevronleftcircle.tsx +11 -0
- package/src/icons/components/IconChevronrightcircle.tsx +11 -0
- package/src/icons/components/IconChevronupcircle.tsx +11 -0
- package/src/icons/components/IconOsnavback.tsx +11 -0
- package/src/icons/components/IconOsnavcenter.tsx +11 -0
- package/src/icons/components/IconOsnavhome.tsx +11 -0
- package/src/icons/components/IconOsnavtask.tsx +11 -0
- package/src/icons/components/IconSignin.tsx +11 -0
- package/src/icons/components/IconSignout.tsx +11 -0
- package/src/icons/components/index.ts +12 -0
- 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").
|
|
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.
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (processed.length === 0)
|
|
274
|
-
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
304
|
-
{/*
|
|
305
|
-
<View style={
|
|
306
|
-
<View
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
{/*
|
|
255
|
+
{/* Table body */}
|
|
337
256
|
<View
|
|
338
257
|
style={{
|
|
339
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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>
|