jfs-components 0.1.18 → 0.1.23
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 +19 -0
- package/lib/commonjs/components/Card/Card.js +29 -12
- package/lib/commonjs/components/CardFeedback/CardFeedback.js +16 -2
- package/lib/commonjs/components/CardInsight/CardInsight.js +34 -13
- package/lib/commonjs/components/CardProviderInfo/CardProviderInfo.js +23 -3
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +31 -8
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +131 -0
- package/lib/commonjs/components/DebitCard/DebitCard.js +23 -3
- package/lib/commonjs/components/HeroSection/HeroSection.js +165 -0
- package/lib/commonjs/components/Image/Image.js +33 -7
- package/lib/commonjs/components/Link/Link.js +115 -0
- package/lib/commonjs/components/ListItem/ListItem.js +16 -1
- package/lib/commonjs/components/MediaCard/MediaCard.js +24 -7
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +34 -13
- package/lib/commonjs/components/TestimonialsCard/TestimonialsCard.js +23 -5
- package/lib/commonjs/components/index.js +21 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +53 -43
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Card/Card.js +31 -14
- package/lib/module/components/CardFeedback/CardFeedback.js +17 -3
- package/lib/module/components/CardInsight/CardInsight.js +36 -15
- package/lib/module/components/CardProviderInfo/CardProviderInfo.js +25 -5
- package/lib/module/components/CheckboxItem/CheckboxItem.js +31 -8
- package/lib/module/components/ContentSheet/ContentSheet.js +126 -0
- package/lib/module/components/DebitCard/DebitCard.js +25 -5
- package/lib/module/components/HeroSection/HeroSection.js +159 -0
- package/lib/module/components/Image/Image.js +34 -7
- package/lib/module/components/Link/Link.js +110 -0
- package/lib/module/components/ListItem/ListItem.js +16 -1
- package/lib/module/components/MediaCard/MediaCard.js +26 -9
- package/lib/module/components/RechargeCard/RechargeCard.js +36 -15
- package/lib/module/components/TestimonialsCard/TestimonialsCard.js +25 -7
- package/lib/module/components/index.js +3 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +53 -43
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Card/Card.d.ts +10 -1
- package/lib/typescript/src/components/CardFeedback/CardFeedback.d.ts +10 -1
- package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +10 -1
- package/lib/typescript/src/components/CardProviderInfo/CardProviderInfo.d.ts +10 -1
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +26 -3
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +71 -0
- package/lib/typescript/src/components/DebitCard/DebitCard.d.ts +10 -1
- package/lib/typescript/src/components/HeroSection/HeroSection.d.ts +80 -0
- package/lib/typescript/src/components/Image/Image.d.ts +27 -2
- package/lib/typescript/src/components/Link/Link.d.ts +73 -0
- package/lib/typescript/src/components/MediaCard/MediaCard.d.ts +10 -1
- package/lib/typescript/src/components/RechargeCard/RechargeCard.d.ts +10 -1
- package/lib/typescript/src/components/TestimonialsCard/TestimonialsCard.d.ts +8 -1
- package/lib/typescript/src/components/index.d.ts +3 -0
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +8 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Card/Card.tsx +46 -15
- package/src/components/CardFeedback/CardFeedback.tsx +29 -4
- package/src/components/CardInsight/CardInsight.tsx +48 -17
- package/src/components/CardProviderInfo/CardProviderInfo.tsx +36 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +59 -14
- package/src/components/ContentSheet/ContentSheet.tsx +217 -0
- package/src/components/DebitCard/DebitCard.tsx +36 -3
- package/src/components/HeroSection/HeroSection.tsx +231 -0
- package/src/components/Image/Image.tsx +55 -3
- package/src/components/Link/Link.tsx +159 -0
- package/src/components/ListItem/ListItem.tsx +15 -0
- package/src/components/MediaCard/MediaCard.tsx +40 -9
- package/src/components/RechargeCard/RechargeCard.tsx +48 -13
- package/src/components/TestimonialsCard/TestimonialsCard.tsx +37 -6
- package/src/components/index.ts +3 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +53 -43
- package/src/icons/registry.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useContext, isValidElement, cloneElement, useMemo } from 'react';
|
|
2
|
-
import { View, Text,
|
|
2
|
+
import { View, Text, Pressable, type ViewStyle, type TextStyle, type StyleProp } from 'react-native';
|
|
3
3
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
4
4
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
5
5
|
import type { Modes } from '../../design-tokens'
|
|
@@ -39,6 +39,15 @@ export interface CardProps {
|
|
|
39
39
|
* Style overrides for the card container.
|
|
40
40
|
*/
|
|
41
41
|
style?: StyleProp<ViewStyle>;
|
|
42
|
+
/**
|
|
43
|
+
* Press handler for the whole card. When set, the card becomes pressable
|
|
44
|
+
* (rendered through a `Pressable` with `accessibilityRole="button"`).
|
|
45
|
+
*/
|
|
46
|
+
onPress?: () => void;
|
|
47
|
+
/** Disable interaction when `onPress` is set. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Accessibility label forwarded to the pressable wrapper. */
|
|
50
|
+
accessibilityLabel?: string;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
|
@@ -63,6 +72,9 @@ export function Card({
|
|
|
63
72
|
modes = EMPTY_MODES,
|
|
64
73
|
mediaAspectRatio = 154 / 116,
|
|
65
74
|
style,
|
|
75
|
+
onPress,
|
|
76
|
+
disabled,
|
|
77
|
+
accessibilityLabel,
|
|
66
78
|
}: CardProps) {
|
|
67
79
|
// Resolve Card Container Tokens
|
|
68
80
|
const backgroundColor = getVariableByName('card/background/color', modes);
|
|
@@ -124,23 +136,42 @@ export function Card({
|
|
|
124
136
|
paddingVertical: contentPaddingVertical,
|
|
125
137
|
};
|
|
126
138
|
|
|
139
|
+
const content = (
|
|
140
|
+
<>
|
|
141
|
+
{header && (
|
|
142
|
+
<View style={headerWrapperStyle}>
|
|
143
|
+
{headerWithModes}
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
{media && (
|
|
147
|
+
<View style={mediaWrapperStyle}>
|
|
148
|
+
{mediaWithModes}
|
|
149
|
+
</View>
|
|
150
|
+
)}
|
|
151
|
+
<View style={contentWrapperStyle}>
|
|
152
|
+
{children}
|
|
153
|
+
</View>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
|
|
127
157
|
return (
|
|
128
158
|
<CardContext.Provider value={{ modes }}>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
{onPress ? (
|
|
160
|
+
<Pressable
|
|
161
|
+
style={[containerStyle, style]}
|
|
162
|
+
onPress={onPress}
|
|
163
|
+
disabled={disabled}
|
|
164
|
+
accessibilityRole="button"
|
|
165
|
+
accessibilityLabel={accessibilityLabel}
|
|
166
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
167
|
+
>
|
|
168
|
+
{content}
|
|
169
|
+
</Pressable>
|
|
170
|
+
) : (
|
|
171
|
+
<View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
|
|
172
|
+
{content}
|
|
142
173
|
</View>
|
|
143
|
-
|
|
174
|
+
)}
|
|
144
175
|
</CardContext.Provider>
|
|
145
176
|
);
|
|
146
177
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useContext, isValidElement, cloneElement } from 'react';
|
|
2
|
-
import { View, Text,
|
|
2
|
+
import { View, Text, Pressable, type ViewStyle, type TextStyle, type StyleProp, Image } from 'react-native';
|
|
3
3
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
4
4
|
import IconComponent from '../../icons/Icon';
|
|
5
5
|
import type { Modes } from '../../design-tokens'
|
|
@@ -22,6 +22,15 @@ export interface CardFeedbackProps {
|
|
|
22
22
|
* Style overrides for the card container.
|
|
23
23
|
*/
|
|
24
24
|
style?: StyleProp<ViewStyle>;
|
|
25
|
+
/**
|
|
26
|
+
* Press handler for the whole card. When set, the card becomes pressable
|
|
27
|
+
* (rendered through a `Pressable` with `accessibilityRole="button"`).
|
|
28
|
+
*/
|
|
29
|
+
onPress?: () => void;
|
|
30
|
+
/** Disable interaction when `onPress` is set. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Accessibility label forwarded to the pressable wrapper. */
|
|
33
|
+
accessibilityLabel?: string;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
/**
|
|
@@ -33,6 +42,9 @@ export function CardFeedback({
|
|
|
33
42
|
children,
|
|
34
43
|
modes: propModes,
|
|
35
44
|
style,
|
|
45
|
+
onPress,
|
|
46
|
+
disabled,
|
|
47
|
+
accessibilityLabel,
|
|
36
48
|
}: CardFeedbackProps) {
|
|
37
49
|
const modes = {
|
|
38
50
|
'Appearance.System': 'positive',
|
|
@@ -61,9 +73,22 @@ export function CardFeedback({
|
|
|
61
73
|
|
|
62
74
|
return (
|
|
63
75
|
<CardFeedbackContext.Provider value={{ modes }}>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
{onPress ? (
|
|
77
|
+
<Pressable
|
|
78
|
+
style={[containerStyle, style]}
|
|
79
|
+
onPress={onPress}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
accessibilityRole="button"
|
|
82
|
+
accessibilityLabel={accessibilityLabel}
|
|
83
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</Pressable>
|
|
87
|
+
) : (
|
|
88
|
+
<View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
|
|
89
|
+
{children}
|
|
90
|
+
</View>
|
|
91
|
+
)}
|
|
67
92
|
</CardFeedbackContext.Provider>
|
|
68
93
|
);
|
|
69
94
|
}
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
+
Pressable,
|
|
5
6
|
type StyleProp,
|
|
6
7
|
type ViewStyle,
|
|
7
8
|
type TextStyle,
|
|
@@ -45,6 +46,15 @@ export type CardInsightProps = {
|
|
|
45
46
|
modes?: Modes
|
|
46
47
|
/** Override container styles. */
|
|
47
48
|
style?: StyleProp<ViewStyle>
|
|
49
|
+
/**
|
|
50
|
+
* Press handler for the whole card. When set, the card becomes pressable
|
|
51
|
+
* (rendered through a `Pressable` with `accessibilityRole="button"`).
|
|
52
|
+
*/
|
|
53
|
+
onPress?: () => void
|
|
54
|
+
/** Disable interaction when `onPress` is set. */
|
|
55
|
+
disabled?: boolean
|
|
56
|
+
/** Accessibility label forwarded to the pressable wrapper (defaults to `title`). */
|
|
57
|
+
accessibilityLabel?: string
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/**
|
|
@@ -67,6 +77,9 @@ function CardInsight({
|
|
|
67
77
|
divider,
|
|
68
78
|
modes = EMPTY_MODES,
|
|
69
79
|
style,
|
|
80
|
+
onPress,
|
|
81
|
+
disabled,
|
|
82
|
+
accessibilityLabel,
|
|
70
83
|
}: CardInsightProps) {
|
|
71
84
|
const background =
|
|
72
85
|
(getVariableByName('cardInsight/background', modes) as string | null) ?? '#ffffff'
|
|
@@ -160,23 +173,20 @@ function CardInsight({
|
|
|
160
173
|
const footerNode = renderFooter()
|
|
161
174
|
const showDivider = footerNode !== null && divider !== false && divider !== null
|
|
162
175
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
style,
|
|
178
|
-
]}
|
|
179
|
-
>
|
|
176
|
+
const containerStyle: ViewStyle = {
|
|
177
|
+
backgroundColor: background,
|
|
178
|
+
borderRadius: radius,
|
|
179
|
+
borderWidth,
|
|
180
|
+
borderColor,
|
|
181
|
+
paddingHorizontal,
|
|
182
|
+
paddingVertical,
|
|
183
|
+
gap,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
alignItems: 'stretch',
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const content = (
|
|
189
|
+
<>
|
|
180
190
|
<View
|
|
181
191
|
style={{
|
|
182
192
|
flexDirection: 'row',
|
|
@@ -237,6 +247,27 @@ function CardInsight({
|
|
|
237
247
|
{showDivider ? renderDivider() : null}
|
|
238
248
|
|
|
239
249
|
{footerNode}
|
|
250
|
+
</>
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if (onPress) {
|
|
254
|
+
return (
|
|
255
|
+
<Pressable
|
|
256
|
+
style={[containerStyle, style]}
|
|
257
|
+
onPress={onPress}
|
|
258
|
+
disabled={disabled}
|
|
259
|
+
accessibilityRole="button"
|
|
260
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
261
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
262
|
+
>
|
|
263
|
+
{content}
|
|
264
|
+
</Pressable>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
|
|
270
|
+
{content}
|
|
240
271
|
</View>
|
|
241
272
|
)
|
|
242
273
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { View, type ViewStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
|
|
2
|
+
import { View, Pressable, type ViewStyle, type StyleProp, type ImageSourcePropType } from 'react-native'
|
|
3
3
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
4
|
import ProductLabel from '../ProductLabel/ProductLabel'
|
|
5
5
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
@@ -16,6 +16,15 @@ export type CardProviderInfoProps = {
|
|
|
16
16
|
modes?: Modes
|
|
17
17
|
/** Override container styles. */
|
|
18
18
|
style?: StyleProp<ViewStyle>
|
|
19
|
+
/**
|
|
20
|
+
* Press handler for the whole card. When set, the card becomes pressable
|
|
21
|
+
* (rendered through a `Pressable` with `accessibilityRole="button"`).
|
|
22
|
+
*/
|
|
23
|
+
onPress?: () => void
|
|
24
|
+
/** Disable interaction when `onPress` is set. */
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
/** Accessibility label forwarded to the pressable wrapper (defaults to `label`). */
|
|
27
|
+
accessibilityLabel?: string
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
/**
|
|
@@ -31,6 +40,9 @@ function CardProviderInfo({
|
|
|
31
40
|
children,
|
|
32
41
|
modes = EMPTY_MODES,
|
|
33
42
|
style,
|
|
43
|
+
onPress,
|
|
44
|
+
disabled,
|
|
45
|
+
accessibilityLabel,
|
|
34
46
|
}: CardProviderInfoProps) {
|
|
35
47
|
const background = getVariableByName('card/providerInfo/background', modes) ?? '#fef4e5'
|
|
36
48
|
const border = getVariableByName('card/providerInfo/border', modes) ?? '#fef4e5'
|
|
@@ -59,8 +71,8 @@ function CardProviderInfo({
|
|
|
59
71
|
rows.push(childArray.slice(i, i + 2))
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
const content = (
|
|
75
|
+
<>
|
|
64
76
|
<ProductLabel label={label} imageSource={imageSource} modes={modes} />
|
|
65
77
|
{childArray.length > 0 && (
|
|
66
78
|
<View style={{ rowGap: gridGapNum }}>
|
|
@@ -75,6 +87,27 @@ function CardProviderInfo({
|
|
|
75
87
|
))}
|
|
76
88
|
</View>
|
|
77
89
|
)}
|
|
90
|
+
</>
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (onPress) {
|
|
94
|
+
return (
|
|
95
|
+
<Pressable
|
|
96
|
+
style={[containerStyle, style]}
|
|
97
|
+
onPress={onPress}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
accessibilityRole="button"
|
|
100
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
101
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
102
|
+
>
|
|
103
|
+
{content}
|
|
104
|
+
</Pressable>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
|
|
110
|
+
{content}
|
|
78
111
|
</View>
|
|
79
112
|
)
|
|
80
113
|
}
|
|
@@ -21,7 +21,25 @@ export type CheckboxItemProps = {
|
|
|
21
21
|
onValueChange?: (checked: boolean) => void
|
|
22
22
|
/** Whether the row is disabled — both the checkbox and row press handler are disabled. */
|
|
23
23
|
disabled?: boolean
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Content rendered in the row's label slot (between the leading and trailing
|
|
26
|
+
* edges). This is the primary way to supply the label and mirrors the Figma
|
|
27
|
+
* "slot" — pass a plain string for the default token-styled label, or any
|
|
28
|
+
* custom node (e.g. a `<Text>`, a row of `<Text>` + `<Badge>`, etc.) for full
|
|
29
|
+
* control. Custom nodes receive the same `modes` as the parent.
|
|
30
|
+
*
|
|
31
|
+
* When both `children` and the legacy {@link label} prop are provided,
|
|
32
|
+
* `children` wins.
|
|
33
|
+
*/
|
|
34
|
+
children?: React.ReactNode
|
|
35
|
+
/**
|
|
36
|
+
* The label content rendered next to the checkbox.
|
|
37
|
+
*
|
|
38
|
+
* @deprecated Prefer the `children` slot instead
|
|
39
|
+
* (`<CheckboxItem>Fixed deposit • 0245</CheckboxItem>`). `label` is kept for
|
|
40
|
+
* backward compatibility and behaves identically — it is used only when no
|
|
41
|
+
* `children` are provided.
|
|
42
|
+
*/
|
|
25
43
|
label?: React.ReactNode
|
|
26
44
|
/**
|
|
27
45
|
* Position of the checkbox control relative to the label.
|
|
@@ -71,13 +89,18 @@ export type CheckboxItemProps = {
|
|
|
71
89
|
* ```tsx
|
|
72
90
|
* const [checked, setChecked] = useState(false)
|
|
73
91
|
*
|
|
92
|
+
* // Recommended: pass the label via the children slot.
|
|
74
93
|
* <CheckboxItem
|
|
75
|
-
* label="Fixed deposit • 0245"
|
|
76
94
|
* checked={checked}
|
|
77
95
|
* onValueChange={setChecked}
|
|
78
96
|
* control="leading"
|
|
79
97
|
* modes={{ 'Color Mode': 'Light' }}
|
|
80
|
-
*
|
|
98
|
+
* >
|
|
99
|
+
* Fixed deposit • 0245
|
|
100
|
+
* </CheckboxItem>
|
|
101
|
+
*
|
|
102
|
+
* // Still supported (deprecated): the `label` prop.
|
|
103
|
+
* <CheckboxItem label="Fixed deposit • 0245" />
|
|
81
104
|
* ```
|
|
82
105
|
*/
|
|
83
106
|
const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
@@ -85,7 +108,8 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
85
108
|
defaultChecked = false,
|
|
86
109
|
onValueChange,
|
|
87
110
|
disabled = false,
|
|
88
|
-
|
|
111
|
+
children,
|
|
112
|
+
label,
|
|
89
113
|
control = 'leading',
|
|
90
114
|
endSlot,
|
|
91
115
|
endSlotWidth = 80,
|
|
@@ -94,6 +118,20 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
94
118
|
labelStyle,
|
|
95
119
|
accessibilityLabel,
|
|
96
120
|
}: CheckboxItemProps, ref: React.Ref<View>) {
|
|
121
|
+
// Label slot resolution — the `children` slot is the primary API; the legacy
|
|
122
|
+
// `label` prop is a backward-compatible fallback. Precedence:
|
|
123
|
+
// 1. `children`, when provided (non-null / non-false).
|
|
124
|
+
// 2. otherwise the legacy `label` prop, whenever it was passed at all —
|
|
125
|
+
// including an explicit `null`/`false`, which (as before) hides the
|
|
126
|
+
// label entirely.
|
|
127
|
+
// 3. otherwise the Figma placeholder, so the default story still renders
|
|
128
|
+
// something meaningful.
|
|
129
|
+
const hasChildren = children != null && children !== false
|
|
130
|
+
const slotContent: React.ReactNode = hasChildren
|
|
131
|
+
? children
|
|
132
|
+
: label !== undefined
|
|
133
|
+
? label
|
|
134
|
+
: 'Fixed deposit • 0245'
|
|
97
135
|
const isTrailing = control === 'trailing'
|
|
98
136
|
const isControlled = controlledChecked !== undefined
|
|
99
137
|
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
@@ -146,7 +184,7 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
146
184
|
}
|
|
147
185
|
|
|
148
186
|
const a11yLabel =
|
|
149
|
-
accessibilityLabel ?? (typeof
|
|
187
|
+
accessibilityLabel ?? (typeof slotContent === 'string' ? slotContent : undefined)
|
|
150
188
|
|
|
151
189
|
const checkboxNode = (
|
|
152
190
|
<Checkbox
|
|
@@ -158,16 +196,23 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
158
196
|
/>
|
|
159
197
|
)
|
|
160
198
|
|
|
199
|
+
// A plain string/number renders with the token-driven label style. Any other
|
|
200
|
+
// node is treated as custom slot content: it fills the label region and
|
|
201
|
+
// receives the parent `modes` (so nested design-system components theme
|
|
202
|
+
// correctly). An explicit null/false hides the label entirely (legacy
|
|
203
|
+
// behaviour of the `label` prop).
|
|
204
|
+
const isTextSlot =
|
|
205
|
+
typeof slotContent === 'string' || typeof slotContent === 'number'
|
|
161
206
|
const labelNode =
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
207
|
+
slotContent == null || slotContent === false ? null : isTextSlot ? (
|
|
208
|
+
<Text style={[resolvedLabelStyle, labelStyle]} selectable={false}>
|
|
209
|
+
{slotContent}
|
|
210
|
+
</Text>
|
|
211
|
+
) : (
|
|
212
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
213
|
+
{cloneChildrenWithModes(slotContent, modes)}
|
|
214
|
+
</View>
|
|
215
|
+
)
|
|
171
216
|
|
|
172
217
|
const endSlotNode = endSlot ? (
|
|
173
218
|
<View
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useContext, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
View,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import Animated, {
|
|
10
|
+
useAnimatedKeyboard,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
} from 'react-native-reanimated'
|
|
13
|
+
import { SafeAreaInsetsContext } from 'react-native-safe-area-context'
|
|
14
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
15
|
+
import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils'
|
|
16
|
+
import type { Modes } from '../../design-tokens'
|
|
17
|
+
|
|
18
|
+
const IS_WEB = Platform.OS === 'web'
|
|
19
|
+
|
|
20
|
+
export type ContentSheetProps = {
|
|
21
|
+
/**
|
|
22
|
+
* Sheet content. The sheet is one big slot: whatever you place here is
|
|
23
|
+
* stacked in a column (separated by the `contentSheet/gap` token) inside the
|
|
24
|
+
* sheet's padding. The sheet has **no fixed height** — it grows and shrinks
|
|
25
|
+
* to fit these children automatically.
|
|
26
|
+
*/
|
|
27
|
+
children?: React.ReactNode
|
|
28
|
+
/**
|
|
29
|
+
* Design-token modes. Cascaded to every slot child via
|
|
30
|
+
* {@link cloneChildrenWithModes} so nested `ListItem`/`Section`/etc. resolve
|
|
31
|
+
* the same theme without you wiring `modes` onto each one.
|
|
32
|
+
*/
|
|
33
|
+
modes?: Modes
|
|
34
|
+
/**
|
|
35
|
+
* Keep the sheet above the on-screen keyboard. When the keyboard opens the
|
|
36
|
+
* sheet rises by exactly the keyboard height; when it closes the sheet drops
|
|
37
|
+
* back. The follow animation is driven entirely on the UI thread
|
|
38
|
+
* (`useAnimatedKeyboard`) so it never schedules a React re-render and stays
|
|
39
|
+
* in perfect sync with the keyboard on both iOS and Android.
|
|
40
|
+
*
|
|
41
|
+
* On web this is a no-op (soft keyboards there don't overlay content).
|
|
42
|
+
* Default `true`.
|
|
43
|
+
*/
|
|
44
|
+
avoidKeyboard?: boolean
|
|
45
|
+
/**
|
|
46
|
+
* Extra gap (px) inserted between the top of the keyboard and the bottom of
|
|
47
|
+
* the sheet while the keyboard is open. Default `0` (sheet sits flush on the
|
|
48
|
+
* keyboard).
|
|
49
|
+
*/
|
|
50
|
+
keyboardSpacing?: number
|
|
51
|
+
/**
|
|
52
|
+
* Add the device's bottom safe-area inset (home indicator) on top of the
|
|
53
|
+
* sheet's bottom padding, so content never sits under the indicator when the
|
|
54
|
+
* sheet rests at the bottom of the screen. Default `true`.
|
|
55
|
+
*/
|
|
56
|
+
safeAreaBottom?: boolean
|
|
57
|
+
/**
|
|
58
|
+
* Pin the sheet to the bottom of its parent (absolute, full width). This is
|
|
59
|
+
* the default behaviour — the sheet "just stays at the bottom".
|
|
60
|
+
*
|
|
61
|
+
* Set to `false` to render the sheet in normal flow instead (e.g. when you
|
|
62
|
+
* compose it inside your own layout container). Keyboard avoidance still
|
|
63
|
+
* works in both modes.
|
|
64
|
+
*/
|
|
65
|
+
pinToBottom?: boolean
|
|
66
|
+
/** Optional style override applied to the sheet container. */
|
|
67
|
+
style?: StyleProp<ViewStyle>
|
|
68
|
+
} & Omit<ViewProps, 'style' | 'children'>
|
|
69
|
+
|
|
70
|
+
interface ContentSheetStyle {
|
|
71
|
+
container: ViewStyle
|
|
72
|
+
paddingBottom: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveContentSheetStyle(modes: Modes): ContentSheetStyle {
|
|
76
|
+
const backgroundColor = getVariableByName('contentSheet/bg', modes) as string
|
|
77
|
+
const gap = getVariableByName('contentSheet/gap', modes) as number
|
|
78
|
+
const paddingHorizontal = getVariableByName('contentSheet/padding/horizontal', modes) as number
|
|
79
|
+
const paddingTop = getVariableByName('contentSheet/padding/top', modes) as number
|
|
80
|
+
const paddingBottom = getVariableByName('contentSheet/padding/bottom', modes) as number
|
|
81
|
+
const radiusTop = getVariableByName('contentSheet/padding/radius/top', modes) as number
|
|
82
|
+
const radiusBottom = getVariableByName('contentSheet/padding/radius/bottom', modes) as number
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
container: {
|
|
86
|
+
backgroundColor,
|
|
87
|
+
gap,
|
|
88
|
+
paddingHorizontal,
|
|
89
|
+
paddingTop,
|
|
90
|
+
// paddingBottom is applied separately so the safe-area inset can be added.
|
|
91
|
+
borderTopLeftRadius: radiusTop,
|
|
92
|
+
borderTopRightRadius: radiusTop,
|
|
93
|
+
borderBottomLeftRadius: radiusBottom,
|
|
94
|
+
borderBottomRightRadius: radiusBottom,
|
|
95
|
+
flexDirection: 'column',
|
|
96
|
+
alignItems: 'stretch',
|
|
97
|
+
overflow: 'hidden',
|
|
98
|
+
},
|
|
99
|
+
paddingBottom,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pinnedStyle: ViewStyle = {
|
|
104
|
+
position: 'absolute',
|
|
105
|
+
left: 0,
|
|
106
|
+
right: 0,
|
|
107
|
+
bottom: 0,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* ContentSheet — a bottom-anchored surface that is essentially one big slot
|
|
112
|
+
* with padding and rounded top corners, mirroring the Figma "Content Sheet".
|
|
113
|
+
*
|
|
114
|
+
* Behaviour highlights:
|
|
115
|
+
* - **Auto height (free & automatic).** The sheet never sets an explicit
|
|
116
|
+
* height. React Native's layout engine (Yoga) sizes it to its children on
|
|
117
|
+
* the native thread, so when the slot content grows or shrinks the sheet
|
|
118
|
+
* follows with zero JS measurement and zero extra re-renders — the most
|
|
119
|
+
* performant option possible.
|
|
120
|
+
* - **Keyboard avoidance** on iOS and Android via `avoidKeyboard` (UI-thread
|
|
121
|
+
* `useAnimatedKeyboard`, no re-renders).
|
|
122
|
+
* - **Bottom pinned** by default; opt out with `pinToBottom={false}`.
|
|
123
|
+
* - **Token-driven** styling via `getVariableByName` + `modes`, with `modes`
|
|
124
|
+
* cascaded to all slot children.
|
|
125
|
+
*/
|
|
126
|
+
function ContentSheet({
|
|
127
|
+
children,
|
|
128
|
+
modes = EMPTY_MODES,
|
|
129
|
+
avoidKeyboard = true,
|
|
130
|
+
keyboardSpacing = 0,
|
|
131
|
+
safeAreaBottom = true,
|
|
132
|
+
pinToBottom = true,
|
|
133
|
+
style,
|
|
134
|
+
...rest
|
|
135
|
+
}: ContentSheetProps) {
|
|
136
|
+
// Read the safe-area inset directly from context (rather than
|
|
137
|
+
// `useSafeAreaInsets`, which throws when no provider is mounted). This keeps
|
|
138
|
+
// the sheet usable without a `SafeAreaProvider` — it simply falls back to 0.
|
|
139
|
+
const safeAreaInsets = useContext(SafeAreaInsetsContext)
|
|
140
|
+
const bottomInset = safeAreaInsets?.bottom ?? 0
|
|
141
|
+
const resolved = useMemo(() => resolveContentSheetStyle(modes), [modes])
|
|
142
|
+
|
|
143
|
+
const processedChildren = useMemo(
|
|
144
|
+
() => cloneChildrenWithModes(flattenChildren(children), modes),
|
|
145
|
+
[children, modes]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const paddingBottom =
|
|
149
|
+
resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0)
|
|
150
|
+
|
|
151
|
+
const containerStyle = useMemo<StyleProp<ViewStyle>>(
|
|
152
|
+
() => [
|
|
153
|
+
pinToBottom ? pinnedStyle : null,
|
|
154
|
+
resolved.container,
|
|
155
|
+
{ paddingBottom },
|
|
156
|
+
style,
|
|
157
|
+
],
|
|
158
|
+
[pinToBottom, resolved.container, paddingBottom, style]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Switching between the keyboard-aware and plain implementation is keyed off
|
|
162
|
+
// `avoidKeyboard` (and platform). Because they are distinct component types,
|
|
163
|
+
// React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
|
|
164
|
+
// inside `KeyboardAwareSheet` always runs in a stable hook order.
|
|
165
|
+
if (avoidKeyboard && !IS_WEB) {
|
|
166
|
+
return (
|
|
167
|
+
<KeyboardAwareSheet
|
|
168
|
+
style={containerStyle}
|
|
169
|
+
spacing={keyboardSpacing}
|
|
170
|
+
{...rest}
|
|
171
|
+
>
|
|
172
|
+
{processedChildren}
|
|
173
|
+
</KeyboardAwareSheet>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<View style={containerStyle} {...rest}>
|
|
179
|
+
{processedChildren}
|
|
180
|
+
</View>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
type KeyboardAwareSheetProps = {
|
|
185
|
+
style: StyleProp<ViewStyle>
|
|
186
|
+
spacing: number
|
|
187
|
+
children: React.ReactNode
|
|
188
|
+
} & Omit<ViewProps, 'style' | 'children'>
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Native-only wrapper that lifts the sheet by the live keyboard height. The
|
|
192
|
+
* translation is computed on the UI thread from `useAnimatedKeyboard`, so the
|
|
193
|
+
* sheet tracks the keyboard frame-for-frame without any JS work.
|
|
194
|
+
*/
|
|
195
|
+
function KeyboardAwareSheet({
|
|
196
|
+
style,
|
|
197
|
+
spacing,
|
|
198
|
+
children,
|
|
199
|
+
...rest
|
|
200
|
+
}: KeyboardAwareSheetProps) {
|
|
201
|
+
const keyboard = useAnimatedKeyboard()
|
|
202
|
+
|
|
203
|
+
const keyboardStyle = useAnimatedStyle(() => {
|
|
204
|
+
const height = keyboard.height.value
|
|
205
|
+
return {
|
|
206
|
+
transform: [{ translateY: height > 0 ? -(height + spacing) : 0 }],
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Animated.View style={[style, keyboardStyle]} {...rest}>
|
|
212
|
+
{children}
|
|
213
|
+
</Animated.View>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default ContentSheet
|