jfs-components 0.0.73 → 0.0.74
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 +23 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
- package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
- package/lib/commonjs/components/FormField/FormField.js +328 -178
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- package/lib/commonjs/components/PageHero/PageHero.js +153 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/Text/Text.js +9 -2
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +60 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/AppBar/AppBar.js +17 -11
- package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
- package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
- package/lib/module/components/Dropdown/Dropdown.js +206 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
- package/lib/module/components/FormField/FormField.js +330 -180
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- package/lib/module/components/PageHero/PageHero.js +147 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/Text/Text.js +9 -2
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +7 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
- package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
- package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
- package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/Text/Text.d.ts +12 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/AppBar/AppBar.tsx +25 -14
- package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
- package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
- package/src/components/Dropdown/Dropdown.tsx +331 -0
- package/src/components/DropdownInput/DropdownInput.tsx +819 -0
- package/src/components/FormField/FormField.tsx +542 -215
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- package/src/components/PageHero/PageHero.tsx +200 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/Text/Text.tsx +24 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +15 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useRef } from 'react';
|
|
4
|
+
import { Image as RNImage, Platform, Pressable, Text, View } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
7
|
+
import Icon from '../../icons/Icon';
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
9
|
+
const IS_IOS = Platform.OS === 'ios';
|
|
10
|
+
const PRESS_DELAY = IS_IOS ? 130 : 0;
|
|
11
|
+
const DEFAULT_IMAGE_RATIO = 125 / 82;
|
|
12
|
+
const pressedOverlayStyle = {
|
|
13
|
+
opacity: 0.7
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Default modes for the "Add" placeholder icon (`iconButton/*` tokens).
|
|
17
|
+
// Caller-supplied `modes` are merged on top so every key here can be overridden.
|
|
18
|
+
const ADD_ICON_DEFAULT_MODES = Object.freeze({
|
|
19
|
+
AppearanceBrand: 'Secondary',
|
|
20
|
+
Emphasis: 'Low',
|
|
21
|
+
"Button / Size": "M"
|
|
22
|
+
});
|
|
23
|
+
const toNumber = (value, fallback) => {
|
|
24
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback;
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
};
|
|
31
|
+
const toFontWeight = (value, fallback) => {
|
|
32
|
+
if (typeof value === 'number') return String(value);
|
|
33
|
+
if (typeof value === 'string') return value;
|
|
34
|
+
return fallback;
|
|
35
|
+
};
|
|
36
|
+
const normalizeImageSource = src => {
|
|
37
|
+
if (src == null) return undefined;
|
|
38
|
+
if (typeof src === 'string') return {
|
|
39
|
+
uri: src
|
|
40
|
+
};
|
|
41
|
+
return src;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `AccountCard` — a compact card preview used to represent a linked
|
|
46
|
+
* financial account in lists / grids.
|
|
47
|
+
*
|
|
48
|
+
* Two visual states are supported via the `state` prop:
|
|
49
|
+
*
|
|
50
|
+
* 1. `'connected'` (default): renders a small card-art preview, a bold
|
|
51
|
+
* `title` and a regular-weight `subtitle` (e.g. masked account number).
|
|
52
|
+
* 2. `'add'`: renders a soft-tinted placeholder field with a centred `+`
|
|
53
|
+
* icon and the `title` underneath, intended as the "add new account"
|
|
54
|
+
* entry-point at the end of a list of connected accounts.
|
|
55
|
+
*
|
|
56
|
+
* All values resolve through the `accountCard/*` design tokens with
|
|
57
|
+
* sensible Figma defaults so the card renders correctly out of the box.
|
|
58
|
+
*
|
|
59
|
+
* @component
|
|
60
|
+
* @param {AccountCardProps} props
|
|
61
|
+
*/
|
|
62
|
+
function AccountCard({
|
|
63
|
+
state = 'connected',
|
|
64
|
+
title = 'Personal account',
|
|
65
|
+
subtitle = '**** 5651',
|
|
66
|
+
imageSource,
|
|
67
|
+
imageRatio = DEFAULT_IMAGE_RATIO,
|
|
68
|
+
cardSlot,
|
|
69
|
+
addIcon = 'ic_add',
|
|
70
|
+
onPress,
|
|
71
|
+
disabled = false,
|
|
72
|
+
modes = EMPTY_MODES,
|
|
73
|
+
style,
|
|
74
|
+
accessibilityLabel,
|
|
75
|
+
accessibilityHint
|
|
76
|
+
}) {
|
|
77
|
+
const iconModes = useMemo(() => modes === EMPTY_MODES ? ADD_ICON_DEFAULT_MODES : {
|
|
78
|
+
...ADD_ICON_DEFAULT_MODES,
|
|
79
|
+
...modes
|
|
80
|
+
}, [modes]);
|
|
81
|
+
|
|
82
|
+
// ---- Tokens ---------------------------------------------------------
|
|
83
|
+
const gap = toNumber(getVariableByName('accountCard/gap', modes), 8);
|
|
84
|
+
const textWrapGap = toNumber(getVariableByName('accountCard/textWrap/gap', modes), 0);
|
|
85
|
+
const titleColor = getVariableByName('accountCard/title/foreground', modes) ?? '#0d0d0f';
|
|
86
|
+
const titleFontFamily = getVariableByName('accountCard/title/fontFamily', modes) ?? 'JioType Var';
|
|
87
|
+
const titleFontSize = toNumber(getVariableByName('accountCard/title/fontSize', modes), 12);
|
|
88
|
+
const titleLineHeight = toNumber(getVariableByName('accountCard/title/lineHeight', modes), 16);
|
|
89
|
+
const titleFontWeight = toFontWeight(getVariableByName('accountCard/title/fontWeight', modes), '700');
|
|
90
|
+
const subtitleColor = getVariableByName('accountCard/subtitle/foreground', modes) ?? '#24262b';
|
|
91
|
+
const subtitleFontFamily = getVariableByName('accountCard/subtitle/fontFamily', modes) ?? 'JioType Var';
|
|
92
|
+
const subtitleFontSize = toNumber(getVariableByName('accountCard/subtitle/fontSize', modes), 12);
|
|
93
|
+
const subtitleLineHeight = toNumber(getVariableByName('accountCard/subtitle/lineHeight', modes), 16);
|
|
94
|
+
const subtitleFontWeight = toFontWeight(getVariableByName('accountCard/subtitle/fontWeight', modes), '400');
|
|
95
|
+
const addFieldRadius = toNumber(getVariableByName('accountCard/addItemField/radius', modes), 8);
|
|
96
|
+
const addFieldBg = getVariableByName('accountCard/addItemField/bg', modes) ?? '#ede8ff';
|
|
97
|
+
const addIconColor = getVariableByName('iconButton/icon/color', iconModes) ?? '#5d00b5';
|
|
98
|
+
const addIconSize = toNumber(getVariableByName('iconButton/icon/size', iconModes), 16);
|
|
99
|
+
|
|
100
|
+
// ---- Styles ---------------------------------------------------------
|
|
101
|
+
const titleStyle = {
|
|
102
|
+
color: titleColor,
|
|
103
|
+
fontFamily: titleFontFamily,
|
|
104
|
+
fontSize: titleFontSize,
|
|
105
|
+
lineHeight: titleLineHeight,
|
|
106
|
+
fontWeight: titleFontWeight,
|
|
107
|
+
width: '100%'
|
|
108
|
+
};
|
|
109
|
+
const subtitleStyle = {
|
|
110
|
+
color: subtitleColor,
|
|
111
|
+
fontFamily: subtitleFontFamily,
|
|
112
|
+
fontSize: subtitleFontSize,
|
|
113
|
+
lineHeight: subtitleLineHeight,
|
|
114
|
+
fontWeight: subtitleFontWeight,
|
|
115
|
+
width: '100%'
|
|
116
|
+
};
|
|
117
|
+
const imageBoxStyle = {
|
|
118
|
+
width: '100%',
|
|
119
|
+
aspectRatio: imageRatio,
|
|
120
|
+
overflow: 'hidden'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// RN's `<Image>` accepts `ImageStyle`, which has a narrower `overflow`
|
|
124
|
+
// union than `ViewStyle`. Build the image-only style separately so the
|
|
125
|
+
// shared box dimensions can be reused without a cast.
|
|
126
|
+
const imageStyle = {
|
|
127
|
+
width: '100%',
|
|
128
|
+
aspectRatio: imageRatio
|
|
129
|
+
};
|
|
130
|
+
const addFieldStyle = {
|
|
131
|
+
...imageBoxStyle,
|
|
132
|
+
backgroundColor: addFieldBg,
|
|
133
|
+
borderRadius: addFieldRadius,
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
justifyContent: 'center'
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ---- Image / placeholder area --------------------------------------
|
|
139
|
+
const renderCardArea = () => {
|
|
140
|
+
if (cardSlot !== undefined && cardSlot !== null) {
|
|
141
|
+
const processed = cloneChildrenWithModes(cardSlot, modes);
|
|
142
|
+
return /*#__PURE__*/_jsx(View, {
|
|
143
|
+
style: imageBoxStyle,
|
|
144
|
+
pointerEvents: "box-none",
|
|
145
|
+
children: processed.length === 1 ? processed[0] : processed
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (state === 'add') {
|
|
149
|
+
return /*#__PURE__*/_jsx(View, {
|
|
150
|
+
style: addFieldStyle,
|
|
151
|
+
accessibilityElementsHidden: true,
|
|
152
|
+
importantForAccessibility: "no",
|
|
153
|
+
children: /*#__PURE__*/_jsx(Icon, {
|
|
154
|
+
name: addIcon,
|
|
155
|
+
size: addIconSize,
|
|
156
|
+
color: addIconColor
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const normalized = normalizeImageSource(imageSource);
|
|
161
|
+
if (normalized) {
|
|
162
|
+
return /*#__PURE__*/_jsx(RNImage, {
|
|
163
|
+
source: normalized,
|
|
164
|
+
style: imageStyle,
|
|
165
|
+
resizeMode: "cover",
|
|
166
|
+
accessibilityElementsHidden: true,
|
|
167
|
+
importantForAccessibility: "no"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return /*#__PURE__*/_jsx(View, {
|
|
171
|
+
style: imageBoxStyle
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// ---- Pressable wiring ----------------------------------------------
|
|
176
|
+
// Keep React state out of the press path. `Pressable`'s style callback
|
|
177
|
+
// applies the pressed visual without a re-render.
|
|
178
|
+
const userHandlersRef = useRef({});
|
|
179
|
+
const handlePressIn = useCallback(e => {
|
|
180
|
+
userHandlersRef.current.onPressIn?.(e);
|
|
181
|
+
}, []);
|
|
182
|
+
const handlePressOut = useCallback(e => {
|
|
183
|
+
userHandlersRef.current.onPressOut?.(e);
|
|
184
|
+
}, []);
|
|
185
|
+
const containerStyle = useMemo(() => ({
|
|
186
|
+
width: '100%',
|
|
187
|
+
flexDirection: 'column',
|
|
188
|
+
alignItems: 'flex-start',
|
|
189
|
+
gap,
|
|
190
|
+
opacity: disabled ? 0.5 : 1
|
|
191
|
+
}), [gap, disabled]);
|
|
192
|
+
const pressableStyle = useCallback(({
|
|
193
|
+
pressed
|
|
194
|
+
}) => [containerStyle, style, pressed && !disabled && onPress ? pressedOverlayStyle : null], [containerStyle, style, disabled, onPress]);
|
|
195
|
+
const showSubtitle = state === 'connected' && subtitle != null && subtitle !== '';
|
|
196
|
+
const a11yRole = onPress ? 'button' : undefined;
|
|
197
|
+
const a11yLabel = accessibilityLabel ?? title;
|
|
198
|
+
const content = /*#__PURE__*/_jsxs(_Fragment, {
|
|
199
|
+
children: [renderCardArea(), title != null && title !== '' || showSubtitle ? /*#__PURE__*/_jsxs(View, {
|
|
200
|
+
style: {
|
|
201
|
+
width: '100%',
|
|
202
|
+
flexDirection: 'column',
|
|
203
|
+
alignItems: 'flex-start',
|
|
204
|
+
gap: textWrapGap
|
|
205
|
+
},
|
|
206
|
+
children: [title != null && title !== '' ? /*#__PURE__*/_jsx(Text, {
|
|
207
|
+
style: titleStyle,
|
|
208
|
+
numberOfLines: 1,
|
|
209
|
+
children: title
|
|
210
|
+
}) : null, showSubtitle ? /*#__PURE__*/_jsx(Text, {
|
|
211
|
+
style: subtitleStyle,
|
|
212
|
+
numberOfLines: 1,
|
|
213
|
+
children: subtitle
|
|
214
|
+
}) : null]
|
|
215
|
+
}) : null]
|
|
216
|
+
});
|
|
217
|
+
if (!onPress) {
|
|
218
|
+
return /*#__PURE__*/_jsx(View, {
|
|
219
|
+
accessibilityLabel: a11yLabel,
|
|
220
|
+
accessibilityHint: accessibilityHint,
|
|
221
|
+
style: [containerStyle, style],
|
|
222
|
+
children: content
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
226
|
+
accessibilityRole: a11yRole,
|
|
227
|
+
accessibilityLabel: a11yLabel,
|
|
228
|
+
accessibilityHint: accessibilityHint,
|
|
229
|
+
accessibilityState: {
|
|
230
|
+
disabled
|
|
231
|
+
},
|
|
232
|
+
onPress: disabled ? undefined : onPress,
|
|
233
|
+
disabled: disabled,
|
|
234
|
+
onPressIn: handlePressIn,
|
|
235
|
+
onPressOut: handlePressOut,
|
|
236
|
+
unstable_pressDelay: PRESS_DELAY,
|
|
237
|
+
style: pressableStyle,
|
|
238
|
+
children: content
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export default /*#__PURE__*/React.memo(AccountCard);
|
|
@@ -42,12 +42,13 @@ export default function AppBar({
|
|
|
42
42
|
const containerStyle = {
|
|
43
43
|
flexDirection: 'row',
|
|
44
44
|
alignItems: 'center',
|
|
45
|
-
justifyContent:
|
|
45
|
+
// No `justifyContent` here: with the inline middle slot using `flex: 1`
|
|
46
|
+
// the three sections lay out naturally (leading | middle | actions).
|
|
47
|
+
// When middleSlot is absent we fall back to `space-between` at the wrapper
|
|
48
|
+
// level so leading & actions still anchor to the edges.
|
|
46
49
|
paddingHorizontal: paddingHorizontal ?? 16,
|
|
47
50
|
paddingVertical: paddingVertical ?? (isMain ? 16 : 10),
|
|
48
51
|
backgroundColor: backgroundColor ?? '#FFFFFF'
|
|
49
|
-
// We can set minHeight if we want to enforce consistency, but padding should dictate it mostly.
|
|
50
|
-
// Figma shows specific heights implicitly via padding + content.
|
|
51
52
|
// MainPage: h=68 (16 top/bot padding? 36 height content?)
|
|
52
53
|
// SubPage: h=52
|
|
53
54
|
};
|
|
@@ -110,8 +111,17 @@ export default function AppBar({
|
|
|
110
111
|
style: actionsStyle,
|
|
111
112
|
children: cloneChildrenWithModes(React.Children.toArray(actionsSlot), modes)
|
|
112
113
|
}) : null;
|
|
114
|
+
|
|
115
|
+
// When there is no middleSlot we want leading & actions pinned to the
|
|
116
|
+
// outer edges, so we apply `space-between` at the wrapper. With a middle
|
|
117
|
+
// slot present, the middle (flex: 1) absorbs the remaining space, so
|
|
118
|
+
// `space-between` is a no-op.
|
|
119
|
+
const wrapperStyle = {
|
|
120
|
+
...containerStyle,
|
|
121
|
+
justifyContent: processedMiddle ? 'flex-start' : 'space-between'
|
|
122
|
+
};
|
|
113
123
|
return /*#__PURE__*/_jsxs(View, {
|
|
114
|
-
style: [
|
|
124
|
+
style: [wrapperStyle, style],
|
|
115
125
|
accessibilityRole: "header",
|
|
116
126
|
accessibilityLabel: undefined,
|
|
117
127
|
...(accessibilityHint ? {
|
|
@@ -126,15 +136,11 @@ export default function AppBar({
|
|
|
126
136
|
children: processedLeading
|
|
127
137
|
}), processedMiddle && /*#__PURE__*/_jsx(View, {
|
|
128
138
|
style: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
right: 0,
|
|
132
|
-
top: 0,
|
|
133
|
-
bottom: 0,
|
|
139
|
+
flex: 1,
|
|
140
|
+
minWidth: 0,
|
|
134
141
|
alignItems: 'center',
|
|
135
142
|
justifyContent: 'center',
|
|
136
|
-
|
|
137
|
-
// Usually middle title shouldn't block actions. `pointerEvents="box-none"` is safer.
|
|
143
|
+
paddingHorizontal: 8
|
|
138
144
|
},
|
|
139
145
|
pointerEvents: "box-none",
|
|
140
146
|
children: processedMiddle
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
4
|
import { View, Text } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -18,6 +18,14 @@ const DEFAULT_ITEMS = [{
|
|
|
18
18
|
label: 'Last updated',
|
|
19
19
|
value: 'Korem ipsum'
|
|
20
20
|
}];
|
|
21
|
+
|
|
22
|
+
// Component-level defaults that match the Figma reference. Caller-provided
|
|
23
|
+
// `modes` are merged on top so every key here can be overridden per-instance.
|
|
24
|
+
const DEFAULT_MODES = Object.freeze({
|
|
25
|
+
'Button / Size': 'S',
|
|
26
|
+
AppearanceBrand: 'Secondary',
|
|
27
|
+
Emphasis: 'Medium'
|
|
28
|
+
});
|
|
21
29
|
const toNumber = (value, fallback) => {
|
|
22
30
|
if (typeof value === 'number') return Number.isFinite(value) ? value : fallback;
|
|
23
31
|
if (typeof value === 'string') {
|
|
@@ -56,10 +64,17 @@ function CardBankAccount({
|
|
|
56
64
|
buttonLabel = 'Button',
|
|
57
65
|
onButtonPress,
|
|
58
66
|
footer,
|
|
59
|
-
modes = EMPTY_MODES,
|
|
67
|
+
modes: propModes = EMPTY_MODES,
|
|
60
68
|
style,
|
|
61
69
|
accessibilityLabel
|
|
62
70
|
}) {
|
|
71
|
+
// Merge caller modes on top of `DEFAULT_MODES` so every default key
|
|
72
|
+
// (e.g. `Button / Size`, `AppearanceBrand`, `Emphasis`) can be overridden
|
|
73
|
+
// per-instance while still applying out of the box.
|
|
74
|
+
const modes = useMemo(() => propModes === EMPTY_MODES ? DEFAULT_MODES : {
|
|
75
|
+
...DEFAULT_MODES,
|
|
76
|
+
...propModes
|
|
77
|
+
}, [propModes]);
|
|
63
78
|
const background = getVariableByName('bankAccountCard/background', modes) ?? '#ffffff';
|
|
64
79
|
const radius = toNumber(getVariableByName('bankAccountCard/radius', modes), 16);
|
|
65
80
|
const paddingHorizontal = toNumber(getVariableByName('bankAccountCard/padding/horizontal', modes), 12);
|
|
@@ -5,12 +5,15 @@ import { View, Text, Pressable } from 'react-native';
|
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
7
7
|
import Checkbox from '../Checkbox/Checkbox';
|
|
8
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
9
|
/**
|
|
10
10
|
* CheckboxItem composes a `Checkbox`, a label and an optional `endSlot` into a
|
|
11
11
|
* single horizontal pressable row. Pressing anywhere on the row (outside of the
|
|
12
12
|
* `endSlot`) toggles the checkbox, mirroring the typical native form pattern.
|
|
13
13
|
*
|
|
14
|
+
* Use the `control` prop to swap the checkbox between the leading (left, default)
|
|
15
|
+
* and trailing (right) edge of the row. The `endSlot` flips to the opposite edge.
|
|
16
|
+
*
|
|
14
17
|
* Mirrors the Figma "Checkbox Item" component and uses the `checkboxItem/*`
|
|
15
18
|
* design tokens for typography and spacing.
|
|
16
19
|
*
|
|
@@ -25,6 +28,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
28
|
* label="Fixed deposit • 0245"
|
|
26
29
|
* checked={checked}
|
|
27
30
|
* onValueChange={setChecked}
|
|
31
|
+
* control="leading"
|
|
28
32
|
* modes={{ 'Color Mode': 'Light' }}
|
|
29
33
|
* />
|
|
30
34
|
* ```
|
|
@@ -35,6 +39,7 @@ function CheckboxItem({
|
|
|
35
39
|
onValueChange,
|
|
36
40
|
disabled = false,
|
|
37
41
|
label = 'Fixed deposit • 0245',
|
|
42
|
+
control = 'leading',
|
|
38
43
|
endSlot,
|
|
39
44
|
endSlotWidth = 80,
|
|
40
45
|
modes = EMPTY_MODES,
|
|
@@ -42,6 +47,7 @@ function CheckboxItem({
|
|
|
42
47
|
labelStyle,
|
|
43
48
|
accessibilityLabel
|
|
44
49
|
}) {
|
|
50
|
+
const isTrailing = control === 'trailing';
|
|
45
51
|
const isControlled = controlledChecked !== undefined;
|
|
46
52
|
const [internalChecked, setInternalChecked] = useState(defaultChecked);
|
|
47
53
|
const isChecked = isControlled ? controlledChecked : internalChecked;
|
|
@@ -80,7 +86,35 @@ function CheckboxItem({
|
|
|
80
86
|
fontWeight: labelFontWeight
|
|
81
87
|
};
|
|
82
88
|
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label : undefined);
|
|
83
|
-
|
|
89
|
+
const checkboxNode = /*#__PURE__*/_jsx(Checkbox, {
|
|
90
|
+
checked: isChecked,
|
|
91
|
+
disabled: disabled,
|
|
92
|
+
onValueChange: handleToggle,
|
|
93
|
+
modes: modes,
|
|
94
|
+
...(a11yLabel !== undefined ? {
|
|
95
|
+
accessibilityLabel: a11yLabel
|
|
96
|
+
} : {})
|
|
97
|
+
});
|
|
98
|
+
const labelNode = label != null && label !== false ? typeof label === 'string' || typeof label === 'number' ? /*#__PURE__*/_jsx(Text, {
|
|
99
|
+
style: [resolvedLabelStyle, labelStyle],
|
|
100
|
+
selectable: false,
|
|
101
|
+
children: label
|
|
102
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
103
|
+
style: {
|
|
104
|
+
flex: 1,
|
|
105
|
+
minWidth: 0
|
|
106
|
+
},
|
|
107
|
+
children: label
|
|
108
|
+
}) : null;
|
|
109
|
+
const endSlotNode = endSlot ? /*#__PURE__*/_jsx(View, {
|
|
110
|
+
style: {
|
|
111
|
+
width: endSlotWidth,
|
|
112
|
+
flexShrink: 0,
|
|
113
|
+
alignItems: 'stretch'
|
|
114
|
+
},
|
|
115
|
+
children: cloneChildrenWithModes(endSlot, modes)
|
|
116
|
+
}) : null;
|
|
117
|
+
return /*#__PURE__*/_jsx(Pressable, {
|
|
84
118
|
style: [containerStyle, style],
|
|
85
119
|
onPress: handleToggle,
|
|
86
120
|
disabled: disabled,
|
|
@@ -90,30 +124,11 @@ function CheckboxItem({
|
|
|
90
124
|
disabled
|
|
91
125
|
},
|
|
92
126
|
accessibilityLabel: a11yLabel,
|
|
93
|
-
children:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
accessibilityLabel: a11yLabel
|
|
99
|
-
}), label != null && label !== false ? typeof label === 'string' || typeof label === 'number' ? /*#__PURE__*/_jsx(Text, {
|
|
100
|
-
style: [resolvedLabelStyle, labelStyle],
|
|
101
|
-
selectable: false,
|
|
102
|
-
children: label
|
|
103
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
104
|
-
style: {
|
|
105
|
-
flex: 1,
|
|
106
|
-
minWidth: 0
|
|
107
|
-
},
|
|
108
|
-
children: label
|
|
109
|
-
}) : null, endSlot ? /*#__PURE__*/_jsx(View, {
|
|
110
|
-
style: {
|
|
111
|
-
width: endSlotWidth,
|
|
112
|
-
flexShrink: 0,
|
|
113
|
-
alignItems: 'stretch'
|
|
114
|
-
},
|
|
115
|
-
children: cloneChildrenWithModes(endSlot, modes)
|
|
116
|
-
}) : null]
|
|
127
|
+
children: isTrailing ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
128
|
+
children: [endSlotNode, labelNode, checkboxNode]
|
|
129
|
+
}) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
130
|
+
children: [checkboxNode, labelNode, endSlotNode]
|
|
131
|
+
})
|
|
117
132
|
});
|
|
118
133
|
}
|
|
119
134
|
export default CheckboxItem;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
import { Platform, Pressable, ScrollView, Text, View } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
8
|
+
import Icon from '../../icons/Icon';
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
const IS_WEB = Platform.OS === 'web';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// DropdownItem
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function useDropdownItemTokens(modes) {
|
|
17
|
+
return useMemo(() => {
|
|
18
|
+
// The `dropdownItem/background` token aliases through the
|
|
19
|
+
// `Dropdown Item State` collection (Idle | Selected), so we resolve
|
|
20
|
+
// both possibilities up-front and pick at render time.
|
|
21
|
+
const idleBackground = getVariableByName('dropdownItem/background', {
|
|
22
|
+
...modes,
|
|
23
|
+
'Dropdown Item State': 'Idle'
|
|
24
|
+
}) || '#ffffff';
|
|
25
|
+
const selectedBackground = getVariableByName('dropdownItem/background', {
|
|
26
|
+
...modes,
|
|
27
|
+
'Dropdown Item State': 'Selected'
|
|
28
|
+
}) || '#f5f5f5';
|
|
29
|
+
const foreground = getVariableByName('dropdownItem/foreground', modes) || '#000000';
|
|
30
|
+
const fontFamily = getVariableByName('dropdownItem/fontFamily', modes) || 'JioType Var';
|
|
31
|
+
const fontSize = parseInt(getVariableByName('dropdownItem/fontSize', modes), 10) || 16;
|
|
32
|
+
const fontWeight = getVariableByName('dropdownItem/fontWeight', modes) || '400';
|
|
33
|
+
const lineHeight = parseInt(getVariableByName('dropdownItem/lineHeight', modes), 10) || 19;
|
|
34
|
+
const gap = parseInt(getVariableByName('dropdownItem/gap', modes), 10) || 8;
|
|
35
|
+
const paddingHorizontal = parseInt(getVariableByName('dropdownItem/padding/horizontal', modes), 10) || 12;
|
|
36
|
+
const paddingVertical = parseInt(getVariableByName('dropdownItem/padding/vertical', modes), 10) || 12;
|
|
37
|
+
return {
|
|
38
|
+
idleBackground,
|
|
39
|
+
selectedBackground,
|
|
40
|
+
foreground,
|
|
41
|
+
fontFamily,
|
|
42
|
+
fontSize,
|
|
43
|
+
fontWeight,
|
|
44
|
+
lineHeight,
|
|
45
|
+
gap,
|
|
46
|
+
paddingHorizontal,
|
|
47
|
+
paddingVertical
|
|
48
|
+
};
|
|
49
|
+
}, [modes]);
|
|
50
|
+
}
|
|
51
|
+
export function DropdownItem({
|
|
52
|
+
label,
|
|
53
|
+
value = null,
|
|
54
|
+
selected = false,
|
|
55
|
+
disabled = false,
|
|
56
|
+
leading,
|
|
57
|
+
trailing,
|
|
58
|
+
onPress,
|
|
59
|
+
children,
|
|
60
|
+
modes: propModes = EMPTY_MODES,
|
|
61
|
+
style,
|
|
62
|
+
labelStyle,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
accessibilityHint
|
|
65
|
+
}) {
|
|
66
|
+
const {
|
|
67
|
+
modes: globalModes
|
|
68
|
+
} = useTokens();
|
|
69
|
+
const modes = useMemo(() => ({
|
|
70
|
+
...globalModes,
|
|
71
|
+
...propModes
|
|
72
|
+
}), [globalModes, propModes]);
|
|
73
|
+
const tokens = useDropdownItemTokens(modes);
|
|
74
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
75
|
+
const handlePress = useCallback(() => {
|
|
76
|
+
if (disabled) return;
|
|
77
|
+
onPress?.(value);
|
|
78
|
+
}, [disabled, onPress, value]);
|
|
79
|
+
const containerStyle = useCallback(({
|
|
80
|
+
pressed
|
|
81
|
+
}) => {
|
|
82
|
+
const showSelected = pressed || isHovered && IS_WEB || selected;
|
|
83
|
+
const base = {
|
|
84
|
+
flexDirection: 'row',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
gap: tokens.gap,
|
|
87
|
+
paddingHorizontal: tokens.paddingHorizontal,
|
|
88
|
+
paddingVertical: tokens.paddingVertical,
|
|
89
|
+
backgroundColor: showSelected ? tokens.selectedBackground : tokens.idleBackground,
|
|
90
|
+
opacity: disabled ? 0.4 : 1,
|
|
91
|
+
width: '100%'
|
|
92
|
+
};
|
|
93
|
+
return [base, style];
|
|
94
|
+
}, [tokens.gap, tokens.paddingHorizontal, tokens.paddingVertical, tokens.idleBackground, tokens.selectedBackground, isHovered, selected, disabled, style]);
|
|
95
|
+
const textStyle = {
|
|
96
|
+
color: tokens.foreground,
|
|
97
|
+
fontFamily: tokens.fontFamily,
|
|
98
|
+
fontSize: tokens.fontSize,
|
|
99
|
+
fontWeight: tokens.fontWeight,
|
|
100
|
+
lineHeight: tokens.lineHeight,
|
|
101
|
+
flexShrink: 1
|
|
102
|
+
};
|
|
103
|
+
const processedLeading = leading ? cloneChildrenWithModes(React.Children.toArray(leading), modes) : null;
|
|
104
|
+
const customTrailing = trailing ? cloneChildrenWithModes(React.Children.toArray(trailing), modes) : null;
|
|
105
|
+
const showDefaultCheck = !trailing && selected;
|
|
106
|
+
const fallbackA11yLabel = accessibilityLabel || (typeof label === 'string' ? label : 'Dropdown item');
|
|
107
|
+
const a11yProps = {
|
|
108
|
+
accessibilityRole: 'menuitem',
|
|
109
|
+
accessibilityLabel: fallbackA11yLabel,
|
|
110
|
+
accessibilityState: {
|
|
111
|
+
selected,
|
|
112
|
+
disabled
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
if (accessibilityHint) {
|
|
116
|
+
a11yProps.accessibilityHint = accessibilityHint;
|
|
117
|
+
}
|
|
118
|
+
const handleHoverIn = useCallback(() => {
|
|
119
|
+
if (IS_WEB && !disabled) setIsHovered(true);
|
|
120
|
+
}, [disabled]);
|
|
121
|
+
const handleHoverOut = useCallback(() => {
|
|
122
|
+
if (IS_WEB) setIsHovered(false);
|
|
123
|
+
}, []);
|
|
124
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
125
|
+
onPress: handlePress,
|
|
126
|
+
disabled: disabled,
|
|
127
|
+
onHoverIn: handleHoverIn,
|
|
128
|
+
onHoverOut: handleHoverOut,
|
|
129
|
+
style: containerStyle,
|
|
130
|
+
...a11yProps,
|
|
131
|
+
children: [processedLeading, children != null ? children : /*#__PURE__*/_jsx(Text, {
|
|
132
|
+
style: [textStyle, labelStyle],
|
|
133
|
+
numberOfLines: 1,
|
|
134
|
+
children: label
|
|
135
|
+
}), customTrailing, showDefaultCheck && /*#__PURE__*/_jsx(Icon, {
|
|
136
|
+
name: "ic_confirm",
|
|
137
|
+
size: 16,
|
|
138
|
+
color: tokens.foreground
|
|
139
|
+
})]
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Dropdown (popup surface)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `Dropdown` is the visual surface (popup) that contains a list of
|
|
149
|
+
* `DropdownItem`s. It is responsible for the background, rounded corners,
|
|
150
|
+
* elevation/shadow, and clipping. Use it standalone for menu UIs, or rely on
|
|
151
|
+
* `DropdownInput` which composes it into a form-field experience.
|
|
152
|
+
*/
|
|
153
|
+
export function Dropdown({
|
|
154
|
+
children,
|
|
155
|
+
maxHeight,
|
|
156
|
+
modes: propModes = EMPTY_MODES,
|
|
157
|
+
style,
|
|
158
|
+
accessibilityLabel
|
|
159
|
+
}) {
|
|
160
|
+
const {
|
|
161
|
+
modes: globalModes
|
|
162
|
+
} = useTokens();
|
|
163
|
+
const modes = useMemo(() => ({
|
|
164
|
+
...globalModes,
|
|
165
|
+
...propModes
|
|
166
|
+
}), [globalModes, propModes]);
|
|
167
|
+
const radius = parseInt(getVariableByName('dropdown/radius', modes), 10) || 8;
|
|
168
|
+
const background = getVariableByName('dropdown/background', modes) || '#ffffff';
|
|
169
|
+
const shadowColor = getVariableByName('dropdown/shadow/color', modes) || 'rgba(0, 0, 0, 0.08)';
|
|
170
|
+
const shadowOffsetX = parseInt(getVariableByName('dropdown/shadow/offsetX', modes), 10) || 0;
|
|
171
|
+
const shadowOffsetY = parseInt(getVariableByName('dropdown/shadow/offsetY', modes), 10) || 4;
|
|
172
|
+
const shadowBlur = parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16;
|
|
173
|
+
const containerStyle = {
|
|
174
|
+
backgroundColor: background,
|
|
175
|
+
borderRadius: radius,
|
|
176
|
+
overflow: 'hidden',
|
|
177
|
+
shadowColor,
|
|
178
|
+
shadowOffset: {
|
|
179
|
+
width: shadowOffsetX,
|
|
180
|
+
height: shadowOffsetY
|
|
181
|
+
},
|
|
182
|
+
shadowOpacity: 1,
|
|
183
|
+
shadowRadius: shadowBlur / 2,
|
|
184
|
+
elevation: 4
|
|
185
|
+
};
|
|
186
|
+
const content = /*#__PURE__*/_jsx(View, {
|
|
187
|
+
style: {
|
|
188
|
+
flexDirection: 'column'
|
|
189
|
+
},
|
|
190
|
+
children: cloneChildrenWithModes(children, modes)
|
|
191
|
+
});
|
|
192
|
+
return /*#__PURE__*/_jsx(View, {
|
|
193
|
+
style: [containerStyle, style],
|
|
194
|
+
accessibilityRole: "menu",
|
|
195
|
+
accessibilityLabel: accessibilityLabel || 'Dropdown menu',
|
|
196
|
+
children: maxHeight != null ? /*#__PURE__*/_jsx(ScrollView, {
|
|
197
|
+
style: {
|
|
198
|
+
maxHeight
|
|
199
|
+
},
|
|
200
|
+
showsVerticalScrollIndicator: true,
|
|
201
|
+
keyboardShouldPersistTaps: "handled",
|
|
202
|
+
children: content
|
|
203
|
+
}) : content
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export default Dropdown;
|