jfs-components 0.0.74 → 0.0.78
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 +109 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
- package/lib/commonjs/components/Avatar/Avatar.js +20 -0
- package/lib/commonjs/components/Badge/Badge.js +23 -0
- package/lib/commonjs/components/Button/Button.js +37 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
- package/lib/commonjs/components/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/ListItem/ListItem.js +25 -10
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
- package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
- package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
- package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PageHero/PageHero.js +41 -5
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/Text/Text.js +31 -1
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/Icon.js +16 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/skeleton/Skeleton.js +234 -0
- package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
- package/lib/commonjs/skeleton/index.js +58 -0
- package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
- package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
- package/lib/module/components/Avatar/Avatar.js +19 -0
- package/lib/module/components/Badge/Badge.js +23 -0
- package/lib/module/components/Button/Button.js +37 -0
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
- package/lib/module/components/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/ListItem/ListItem.js +25 -10
- package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
- package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
- package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
- package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PageHero/PageHero.js +41 -5
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/Text/Text.js +31 -1
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +5 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/Icon.js +16 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/skeleton/Skeleton.js +229 -0
- package/lib/module/skeleton/SkeletonGroup.js +133 -0
- package/lib/module/skeleton/index.js +6 -0
- package/lib/module/skeleton/shimmer-tokens.js +181 -0
- package/lib/module/skeleton/useReducedMotion.js +61 -0
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
- package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
- package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
- package/lib/typescript/src/components/Button/Button.d.ts +8 -1
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
- package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
- package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
- package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/Text/Text.d.ts +20 -1
- package/lib/typescript/src/components/index.d.ts +8 -3
- package/lib/typescript/src/icons/Icon.d.ts +7 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
- package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
- package/lib/typescript/src/skeleton/index.d.ts +5 -0
- package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
- package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
- package/package.json +11 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +210 -92
- package/src/components/Avatar/Avatar.tsx +26 -0
- package/src/components/Badge/Badge.tsx +27 -0
- package/src/components/Button/Button.tsx +40 -0
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/ListItem/ListItem.tsx +21 -10
- package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
- package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
- package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
- package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PageHero/PageHero.tsx +61 -4
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/Text/Text.tsx +54 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +8 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/Icon.tsx +17 -0
- package/src/icons/registry.ts +1 -1
- package/src/index.ts +1 -0
- package/src/skeleton/Skeleton.tsx +298 -0
- package/src/skeleton/SkeletonGroup.tsx +193 -0
- package/src/skeleton/index.ts +10 -0
- package/src/skeleton/shimmer-tokens.ts +221 -0
- package/src/skeleton/useReducedMotion.ts +72 -0
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
} from 'react-native'
|
|
15
15
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
16
16
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
17
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
18
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
17
19
|
|
|
18
20
|
const avatarImage = require('./31595e70c4181263f9971590224b12934b280c9b.png')
|
|
19
21
|
|
|
@@ -134,6 +136,12 @@ export type AvatarProps = {
|
|
|
134
136
|
accessibilityLabel?: string;
|
|
135
137
|
onPress?: () => void;
|
|
136
138
|
disabled?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
141
|
+
* same-size circular skeleton instead of the avatar. Defaults to
|
|
142
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
143
|
+
*/
|
|
144
|
+
loading?: boolean;
|
|
137
145
|
} & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityRole' | 'accessibilityLabel'>;
|
|
138
146
|
|
|
139
147
|
function Avatar({
|
|
@@ -145,11 +153,17 @@ function Avatar({
|
|
|
145
153
|
// component intentionally renders `accessibilityLabel={undefined}` on the
|
|
146
154
|
// wrapper (the inner Text/Image carry the label instead).
|
|
147
155
|
accessibilityLabel: _accessibilityLabel,
|
|
156
|
+
loading,
|
|
148
157
|
...rest
|
|
149
158
|
}: AvatarProps) {
|
|
150
159
|
const isMonogram = style === 'Monogram'
|
|
151
160
|
const tokens = useMemo(() => resolveAvatarTokens(modes, isMonogram), [modes, isMonogram])
|
|
152
161
|
|
|
162
|
+
// Skeleton context — read unconditionally; the actual short-circuit
|
|
163
|
+
// happens AFTER all remaining hooks below.
|
|
164
|
+
const { active: groupActive } = useSkeleton()
|
|
165
|
+
const isLoading = loading ?? groupActive
|
|
166
|
+
|
|
153
167
|
// Focus is a sustained visible state — keep mirroring on web; gate the
|
|
154
168
|
// setter so it never fires on native (where focus events don't fire on
|
|
155
169
|
// these elements anyway).
|
|
@@ -197,6 +211,18 @@ function Avatar({
|
|
|
197
211
|
[tokens.containerStyle, isFocused]
|
|
198
212
|
)
|
|
199
213
|
|
|
214
|
+
if (isLoading) {
|
|
215
|
+
const size = tokens.containerStyle.width as number
|
|
216
|
+
return (
|
|
217
|
+
<Skeleton
|
|
218
|
+
kind="other"
|
|
219
|
+
width={size}
|
|
220
|
+
height={size}
|
|
221
|
+
modes={modes}
|
|
222
|
+
/>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
200
226
|
// The inner content varies; everything else (wrapper, handlers, style) is shared.
|
|
201
227
|
const innerContent = isMonogram ? (
|
|
202
228
|
<View style={monogramContainerStyle}>
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from 'react-native'
|
|
9
9
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
10
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
11
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
12
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
11
13
|
|
|
12
14
|
type BadgeProps = {
|
|
13
15
|
/** Visible label text shown inside the badge */
|
|
@@ -19,6 +21,12 @@ type BadgeProps = {
|
|
|
19
21
|
accessibilityLabel?: string
|
|
20
22
|
style?: ViewStyle
|
|
21
23
|
labelStyle?: TextStyle
|
|
24
|
+
/**
|
|
25
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
26
|
+
* badge-shaped skeleton placeholder instead of the badge. Defaults to
|
|
27
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
28
|
+
*/
|
|
29
|
+
loading?: boolean
|
|
22
30
|
} & Omit<React.ComponentProps<typeof View>, 'style' | 'accessibilityLabel' | 'accessibilityRole'>
|
|
23
31
|
|
|
24
32
|
function Badge({
|
|
@@ -28,6 +36,7 @@ function Badge({
|
|
|
28
36
|
accessibilityLabel,
|
|
29
37
|
style,
|
|
30
38
|
labelStyle,
|
|
39
|
+
loading,
|
|
31
40
|
...rest
|
|
32
41
|
}: BadgeProps) {
|
|
33
42
|
// Resolve token values (fall back to sensible defaults)
|
|
@@ -51,6 +60,24 @@ function Badge({
|
|
|
51
60
|
Number(getVariableByName('badge/label/lineHeight', modes) as unknown) ||
|
|
52
61
|
Math.round(fontSize * 1.2)
|
|
53
62
|
|
|
63
|
+
// Skeleton short-circuit. Size derived from the same tokens the loaded
|
|
64
|
+
// badge would use so the placeholder occupies the same box.
|
|
65
|
+
const { active: groupActive } = useSkeleton()
|
|
66
|
+
const isLoading = loading ?? groupActive
|
|
67
|
+
if (isLoading) {
|
|
68
|
+
const charWidth = fontSize * 0.55
|
|
69
|
+
const labelWidth = Math.max(label.length, 3) * charWidth
|
|
70
|
+
return (
|
|
71
|
+
<Skeleton
|
|
72
|
+
kind="badge"
|
|
73
|
+
width={paddingHorizontal * 2 + labelWidth}
|
|
74
|
+
height={paddingVertical * 2 + lineHeight}
|
|
75
|
+
style={{ alignSelf: 'flex-start' }}
|
|
76
|
+
modes={modes}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
const Container: any = onPress ? Pressable : View
|
|
55
82
|
|
|
56
83
|
const containerStyle: ViewStyle = {
|
|
@@ -14,6 +14,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
|
14
14
|
import { usePressableWebSupport, type SafePressableProps, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
15
15
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
16
16
|
import Icon from '../../icons/Icon'
|
|
17
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
18
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
17
19
|
|
|
18
20
|
export type ButtonProps = SafePressableProps & {
|
|
19
21
|
label?: string;
|
|
@@ -54,6 +56,13 @@ export type ButtonProps = SafePressableProps & {
|
|
|
54
56
|
* Web-specific accessibility props (only used on web platform)
|
|
55
57
|
*/
|
|
56
58
|
webAccessibilityProps?: WebAccessibilityProps;
|
|
59
|
+
/**
|
|
60
|
+
* Explicit per-instance loading override. When `true`, the button renders
|
|
61
|
+
* as a pill-shaped skeleton of the same size; when `false`, the
|
|
62
|
+
* surrounding `<SkeletonGroup>` is ignored. Defaults to inheriting from
|
|
63
|
+
* the group.
|
|
64
|
+
*/
|
|
65
|
+
loading?: boolean;
|
|
57
66
|
};
|
|
58
67
|
|
|
59
68
|
// ---------------------------------------------------------------------------
|
|
@@ -224,6 +233,7 @@ function ButtonImpl({
|
|
|
224
233
|
accessibilityHint,
|
|
225
234
|
accessibilityState,
|
|
226
235
|
webAccessibilityProps,
|
|
236
|
+
loading,
|
|
227
237
|
...rest
|
|
228
238
|
}: ButtonProps) {
|
|
229
239
|
// Hover state is web-only in practice; the setter is gated so native taps
|
|
@@ -248,6 +258,12 @@ function ButtonImpl({
|
|
|
248
258
|
[modes, disabled]
|
|
249
259
|
)
|
|
250
260
|
|
|
261
|
+
// Skeleton context — read unconditionally so React's hook order stays
|
|
262
|
+
// stable. The actual short-circuit return happens AFTER all remaining
|
|
263
|
+
// hooks have been called below.
|
|
264
|
+
const { active: groupActive } = useSkeleton()
|
|
265
|
+
const isLoading = loading ?? groupActive
|
|
266
|
+
|
|
251
267
|
// Active label color: base by default; hover override (web-only) when hovered.
|
|
252
268
|
// Press color is intentionally NOT applied to the label on native — applying
|
|
253
269
|
// it would require a React render per touch and re-introduce the flicker.
|
|
@@ -354,6 +370,30 @@ function ButtonImpl({
|
|
|
354
370
|
}
|
|
355
371
|
}
|
|
356
372
|
|
|
373
|
+
if (isLoading) {
|
|
374
|
+
const { container, baseLabel, iconSize, accessoryOffset } = tokens
|
|
375
|
+
const paddingHorizontal = (container.paddingHorizontal as number) ?? 20
|
|
376
|
+
const paddingVertical = (container.paddingVertical as number) ?? 12
|
|
377
|
+
const lineHeight = (baseLabel.lineHeight as number) ?? 19
|
|
378
|
+
const fontSize = (baseLabel.fontSize as number) ?? 16
|
|
379
|
+
const labelText = typeof label === 'string' ? label : 'Button'
|
|
380
|
+
const charWidth = fontSize * 0.55
|
|
381
|
+
const labelWidth = Math.max(labelText.length, 4) * charWidth
|
|
382
|
+
const hasAccessory = !!(leading || trailing || icon)
|
|
383
|
+
const accessoryWidth = hasAccessory ? iconSize + accessoryOffset * 2 : 0
|
|
384
|
+
const skeletonWidth = paddingHorizontal * 2 + labelWidth + accessoryWidth
|
|
385
|
+
const skeletonHeight = paddingVertical * 2 + lineHeight
|
|
386
|
+
return (
|
|
387
|
+
<Skeleton
|
|
388
|
+
kind="other"
|
|
389
|
+
width={skeletonWidth}
|
|
390
|
+
height={skeletonHeight}
|
|
391
|
+
style={style as any}
|
|
392
|
+
modes={modes}
|
|
393
|
+
/>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
357
397
|
return (
|
|
358
398
|
<Pressable
|
|
359
399
|
accessibilityRole="button"
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
Platform,
|
|
5
|
+
View,
|
|
5
6
|
type StyleProp,
|
|
6
7
|
type ViewStyle,
|
|
7
8
|
} from 'react-native'
|
|
@@ -50,6 +51,16 @@ function useFocusVisible() {
|
|
|
50
51
|
return { isFocusVisible, focusHandlers: { onFocus, onBlur } }
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Minimum touch target per iOS HIG / Material accessibility guidance. */
|
|
55
|
+
const MIN_TOUCH_TARGET = 44
|
|
56
|
+
|
|
57
|
+
const touchTargetStyle: ViewStyle = {
|
|
58
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
59
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
export interface CheckboxProps {
|
|
54
65
|
/** Whether the checkbox is checked (controlled) */
|
|
55
66
|
checked?: boolean
|
|
@@ -207,7 +218,7 @@ function Checkbox({
|
|
|
207
218
|
|
|
208
219
|
return (
|
|
209
220
|
<Pressable
|
|
210
|
-
style={[
|
|
221
|
+
style={[touchTargetStyle, style]}
|
|
211
222
|
onPress={handlePress}
|
|
212
223
|
disabled={disabled}
|
|
213
224
|
onHoverIn={() => setIsHovered(true)}
|
|
@@ -217,14 +228,16 @@ function Checkbox({
|
|
|
217
228
|
accessibilityState={{ checked: isChecked, disabled }}
|
|
218
229
|
accessibilityLabel={accessibilityLabel}
|
|
219
230
|
>
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
<View style={resolveStyle()}>
|
|
232
|
+
{isChecked && (
|
|
233
|
+
<Svg width={12} height={9} viewBox="0 0 12 9" fill="none">
|
|
234
|
+
<Path
|
|
235
|
+
d="M4.00091 8.66939C3.91321 8.6699 3.82628 8.65309 3.74509 8.61991C3.6639 8.58673 3.59006 8.53785 3.52779 8.47606L0.195972 5.14273C0.0704931 5.01719 -1.86978e-09 4.84693 0 4.66939C1.86978e-09 4.49186 0.0704931 4.3216 0.195972 4.19606C0.321451 4.07053 0.491636 4 0.66909 4C0.846544 4 1.01673 4.07053 1.14221 4.19606L4.00091 7.06273L10.8578 0.196061C10.9833 0.0705253 11.1535 0 11.3309 0C11.5084 0 11.6785 0.0705253 11.804 0.196061C11.9295 0.321597 12 0.49186 12 0.669394C12 0.846929 11.9295 1.01719 11.804 1.14273L4.47403 8.47606C4.41176 8.53785 4.33792 8.58673 4.25673 8.61991C4.17554 8.65309 4.08861 8.6699 4.00091 8.66939Z"
|
|
236
|
+
fill={markColor}
|
|
237
|
+
/>
|
|
238
|
+
</Svg>
|
|
239
|
+
)}
|
|
240
|
+
</View>
|
|
228
241
|
</Pressable>
|
|
229
242
|
)
|
|
230
243
|
}
|
|
@@ -162,51 +162,60 @@ function useChevronTokens(modes: Record<string, any>) {
|
|
|
162
162
|
}, [modes])
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function toNumber(value: unknown, fallback: number): number {
|
|
166
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
167
|
+
if (typeof value === 'string') {
|
|
168
|
+
const parsed = parseFloat(value)
|
|
169
|
+
if (Number.isFinite(parsed)) return parsed
|
|
170
|
+
}
|
|
171
|
+
return fallback
|
|
172
|
+
}
|
|
173
|
+
|
|
165
174
|
function useFormFieldTokens(modes: Record<string, any>) {
|
|
166
175
|
return useMemo(() => {
|
|
167
176
|
const labelColor =
|
|
168
177
|
(getVariableByName('formField/label/color', modes) as string) ||
|
|
169
|
-
'#
|
|
178
|
+
'#000000'
|
|
170
179
|
const labelFontFamily =
|
|
171
180
|
(getVariableByName('formField/label/fontFamily', modes) as string) ||
|
|
172
181
|
'JioType Var'
|
|
173
|
-
const labelFontSize =
|
|
174
|
-
|
|
182
|
+
const labelFontSize = toNumber(
|
|
183
|
+
getVariableByName('formField/label/fontSize', modes),
|
|
175
184
|
14
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
)
|
|
186
|
+
const labelLineHeight = toNumber(
|
|
187
|
+
getVariableByName('formField/label/lineHeight', modes),
|
|
188
|
+
17
|
|
189
|
+
)
|
|
181
190
|
const labelFontWeight =
|
|
182
191
|
(getVariableByName('formField/label/fontWeight', modes) as string) ||
|
|
183
192
|
'500'
|
|
184
193
|
|
|
185
|
-
const gap =
|
|
186
|
-
|
|
187
|
-
const inputPaddingH =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
parseInt(getVariableByName('formField/input/gap', modes), 10) || 8
|
|
194
|
-
const inputRadius =
|
|
195
|
-
parseInt(getVariableByName('formField/input/radius', modes), 10) ||
|
|
194
|
+
const gap = toNumber(getVariableByName('formField/gap', modes), 8)
|
|
195
|
+
|
|
196
|
+
const inputPaddingH = toNumber(
|
|
197
|
+
getVariableByName('formField/input/padding/horizontal', modes),
|
|
198
|
+
12
|
|
199
|
+
)
|
|
200
|
+
const inputGap = toNumber(
|
|
201
|
+
getVariableByName('formField/input/gap', modes),
|
|
196
202
|
8
|
|
203
|
+
)
|
|
204
|
+
const inputRadius = toNumber(
|
|
205
|
+
getVariableByName('formField/input/radius', modes),
|
|
206
|
+
8
|
|
207
|
+
)
|
|
197
208
|
const inputBackground =
|
|
198
209
|
(getVariableByName('formField/input/background', modes) as string) ||
|
|
199
210
|
'#ffffff'
|
|
200
|
-
const inputFontSize =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
10
|
|
209
|
-
) || 45
|
|
211
|
+
const inputFontSize = toNumber(
|
|
212
|
+
getVariableByName('formField/input/label/fontSize', modes),
|
|
213
|
+
16
|
|
214
|
+
)
|
|
215
|
+
const inputLineHeight = toNumber(
|
|
216
|
+
getVariableByName('formField/input/label/lineHeight', modes),
|
|
217
|
+
45
|
|
218
|
+
)
|
|
210
219
|
const inputFontFamily =
|
|
211
220
|
(getVariableByName(
|
|
212
221
|
'formField/input/label/fontFamily',
|
|
@@ -231,11 +240,13 @@ function useFormFieldTokens(modes: Record<string, any>) {
|
|
|
231
240
|
) as string) ||
|
|
232
241
|
(getVariableByName('formField/input/border/color', modes) as string) ||
|
|
233
242
|
'#b5b6b7'
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
243
|
+
// Figma spec: 1.5px. Using parseFloat (via toNumber) preserves the
|
|
244
|
+
// fractional value — parseInt was truncating it to 1, leaving the
|
|
245
|
+
// resolved row height ~1px shorter than the Figma reference.
|
|
246
|
+
const inputBorderSize = toNumber(
|
|
247
|
+
getVariableByName('formField/input/border/size', modes),
|
|
248
|
+
1.5
|
|
249
|
+
)
|
|
239
250
|
|
|
240
251
|
return {
|
|
241
252
|
labelColor,
|
|
@@ -314,7 +325,7 @@ function DropdownInput({
|
|
|
314
325
|
supportText,
|
|
315
326
|
errorMessage,
|
|
316
327
|
menuMaxHeight = 240,
|
|
317
|
-
menuOffset =
|
|
328
|
+
menuOffset = 6,
|
|
318
329
|
matchTriggerWidth = true,
|
|
319
330
|
closeOnBackdropPress = true,
|
|
320
331
|
modes: propModes = EMPTY_MODES,
|
|
@@ -594,19 +605,23 @@ function DropdownInput({
|
|
|
594
605
|
}
|
|
595
606
|
|
|
596
607
|
// Focus ring uses the resolved input border color from FormField States so
|
|
597
|
-
// active/error look consistent with TextInput-based FormField.
|
|
598
|
-
//
|
|
608
|
+
// active/error look consistent with TextInput-based FormField. Only the
|
|
609
|
+
// color changes between states — width stays constant to avoid layout
|
|
610
|
+
// shift when opening the menu (a shift would invalidate the measured
|
|
611
|
+
// trigger rect and visually shove the popup).
|
|
599
612
|
const inputRowStyle: ViewStyle = {
|
|
600
613
|
flexDirection: 'row',
|
|
601
614
|
alignItems: 'center',
|
|
602
615
|
backgroundColor: tokens.inputBackground,
|
|
603
616
|
borderColor: tokens.inputBorderColor,
|
|
604
|
-
borderWidth:
|
|
617
|
+
borderWidth: tokens.inputBorderSize,
|
|
618
|
+
borderStyle: 'solid',
|
|
605
619
|
borderRadius: tokens.inputRadius,
|
|
606
620
|
paddingHorizontal: tokens.inputPaddingH,
|
|
607
621
|
paddingVertical: 0,
|
|
608
622
|
gap: tokens.inputGap,
|
|
609
623
|
minHeight: tokens.inputLineHeight,
|
|
624
|
+
width: '100%',
|
|
610
625
|
}
|
|
611
626
|
|
|
612
627
|
const valueTextStyle: TextStyle = {
|
|
@@ -763,12 +778,25 @@ function DropdownInput({
|
|
|
763
778
|
/>
|
|
764
779
|
)}
|
|
765
780
|
|
|
781
|
+
{/*
|
|
782
|
+
IMPORTANT: do NOT pass `statusBarTranslucent` to this Modal.
|
|
783
|
+
On Android, a `statusBarTranslucent` Modal opens its own window
|
|
784
|
+
that spans the entire screen (origin at screen-top, including
|
|
785
|
+
the status bar), but `measureInWindow` on the trigger returns
|
|
786
|
+
coordinates relative to the *activity* window — which on a
|
|
787
|
+
default Android setup starts BELOW the status bar. The two
|
|
788
|
+
coordinate spaces then differ by `StatusBar.currentHeight`, so
|
|
789
|
+
`triggerRect.y + triggerRect.height + menuOffset` lands roughly
|
|
790
|
+
one status-bar-height ABOVE the visible input, making the
|
|
791
|
+
popup overlap the input row. Leaving `statusBarTranslucent`
|
|
792
|
+
off keeps the Modal's window aligned with the activity
|
|
793
|
+
window, which is what every measurement here assumes.
|
|
794
|
+
*/}
|
|
766
795
|
<Modal
|
|
767
796
|
visible={isOpen}
|
|
768
797
|
transparent
|
|
769
798
|
animationType="fade"
|
|
770
799
|
onRequestClose={closeMenu}
|
|
771
|
-
statusBarTranslucent
|
|
772
800
|
>
|
|
773
801
|
<Pressable
|
|
774
802
|
style={StyleSheet.absoluteFill}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
type StyleProp,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
10
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
11
|
+
import Checkbox from '../Checkbox/Checkbox'
|
|
12
|
+
import Button from '../Button/Button'
|
|
13
|
+
|
|
14
|
+
export type ExpandableCheckboxProps = {
|
|
15
|
+
/** Long text label rendered next to the checkbox. */
|
|
16
|
+
label?: string
|
|
17
|
+
/** Whether the checkbox is checked (controlled). */
|
|
18
|
+
checked?: boolean
|
|
19
|
+
/** Initial checked state (uncontrolled). */
|
|
20
|
+
defaultChecked?: boolean
|
|
21
|
+
/** Callback fired when the checked state changes. */
|
|
22
|
+
onValueChange?: (checked: boolean) => void
|
|
23
|
+
/** Whether the row is expanded to reveal the full label (controlled). */
|
|
24
|
+
expanded?: boolean
|
|
25
|
+
/** Initial expanded state (uncontrolled). Defaults to `false` (Idle). */
|
|
26
|
+
defaultExpanded?: boolean
|
|
27
|
+
/** Callback fired when the expanded state changes. */
|
|
28
|
+
onExpandedChange?: (expanded: boolean) => void
|
|
29
|
+
/** Whether the entire row is disabled. */
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
/** Label for the toggle button shown when the row is collapsed. */
|
|
32
|
+
readMoreLabel?: string
|
|
33
|
+
/** Label for the toggle button shown when the row is expanded. */
|
|
34
|
+
readLessLabel?: string
|
|
35
|
+
/** Number of lines to show when collapsed. Defaults to `1`. */
|
|
36
|
+
collapsedLines?: number
|
|
37
|
+
/** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
|
|
38
|
+
modes?: Record<string, any>
|
|
39
|
+
/** Override outer container styles. */
|
|
40
|
+
style?: StyleProp<ViewStyle>
|
|
41
|
+
/** Override the label text styles. */
|
|
42
|
+
labelStyle?: StyleProp<TextStyle>
|
|
43
|
+
/** Accessibility label for the checkbox. Falls back to `label`. */
|
|
44
|
+
accessibilityLabel?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default modes applied to the inner toggle `Button`. These resolve the
|
|
49
|
+
* tertiary-style pill in the Figma reference (small, transparent background,
|
|
50
|
+
* brand purple foreground). Any value supplied via the consumer `modes` prop
|
|
51
|
+
* takes precedence over these defaults.
|
|
52
|
+
*/
|
|
53
|
+
const BUTTON_DEFAULT_MODES = {
|
|
54
|
+
'Button / Size': 'XS',
|
|
55
|
+
AppearanceBrand: 'Secondary',
|
|
56
|
+
Emphasis: 'Low',
|
|
57
|
+
} as const
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ExpandableCheckbox composes a `Checkbox`, a long-form label and a
|
|
61
|
+
* "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
|
|
62
|
+
* component with two states:
|
|
63
|
+
*
|
|
64
|
+
* - **Idle (collapsed)** — checkbox + truncated label + toggle button arranged
|
|
65
|
+
* in a horizontal row (cross-axis centered).
|
|
66
|
+
* - **Open (expanded)** — checkbox + full multi-line label, with the toggle
|
|
67
|
+
* button right-aligned beneath the row.
|
|
68
|
+
*
|
|
69
|
+
* The checkbox and the toggle button have independent press handlers — pressing
|
|
70
|
+
* the toggle does not affect the checked state, and toggling the checkbox does
|
|
71
|
+
* not collapse / expand the row.
|
|
72
|
+
*
|
|
73
|
+
* @component
|
|
74
|
+
* @param {ExpandableCheckboxProps} props
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* <ExpandableCheckbox
|
|
79
|
+
* label="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
|
|
80
|
+
* defaultChecked
|
|
81
|
+
* onValueChange={setAccepted}
|
|
82
|
+
* modes={{ 'Color Mode': 'Light' }}
|
|
83
|
+
* />
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
function ExpandableCheckbox({
|
|
87
|
+
label = '',
|
|
88
|
+
checked: controlledChecked,
|
|
89
|
+
defaultChecked = false,
|
|
90
|
+
onValueChange,
|
|
91
|
+
expanded: controlledExpanded,
|
|
92
|
+
defaultExpanded = false,
|
|
93
|
+
onExpandedChange,
|
|
94
|
+
disabled = false,
|
|
95
|
+
readMoreLabel = 'Read more',
|
|
96
|
+
readLessLabel = 'Read less',
|
|
97
|
+
collapsedLines = 1,
|
|
98
|
+
modes = EMPTY_MODES,
|
|
99
|
+
style,
|
|
100
|
+
labelStyle,
|
|
101
|
+
accessibilityLabel,
|
|
102
|
+
}: ExpandableCheckboxProps) {
|
|
103
|
+
const isCheckedControlled = controlledChecked !== undefined
|
|
104
|
+
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
105
|
+
const isChecked = isCheckedControlled ? controlledChecked : internalChecked
|
|
106
|
+
|
|
107
|
+
const isExpandedControlled = controlledExpanded !== undefined
|
|
108
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded)
|
|
109
|
+
const isExpanded = isExpandedControlled ? controlledExpanded : internalExpanded
|
|
110
|
+
|
|
111
|
+
const handleToggleChecked = useCallback(
|
|
112
|
+
(next: boolean) => {
|
|
113
|
+
if (disabled) return
|
|
114
|
+
if (!isCheckedControlled) setInternalChecked(next)
|
|
115
|
+
onValueChange?.(next)
|
|
116
|
+
},
|
|
117
|
+
[disabled, isCheckedControlled, onValueChange]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const handleToggleExpanded = useCallback(() => {
|
|
121
|
+
if (disabled) return
|
|
122
|
+
const next = !isExpanded
|
|
123
|
+
if (!isExpandedControlled) setInternalExpanded(next)
|
|
124
|
+
onExpandedChange?.(next)
|
|
125
|
+
}, [disabled, isExpanded, isExpandedControlled, onExpandedChange])
|
|
126
|
+
|
|
127
|
+
const gap =
|
|
128
|
+
(getVariableByName('expandableCheckbox/gap', modes) as number | null) ?? 8
|
|
129
|
+
|
|
130
|
+
const rowGap =
|
|
131
|
+
(getVariableByName('checkboxItem/gap', modes) as number | null) ?? 8
|
|
132
|
+
const rowPaddingHorizontal =
|
|
133
|
+
(getVariableByName('checkboxItem/padding/horizontal', modes) as number | null) ?? 0
|
|
134
|
+
const rowPaddingVertical =
|
|
135
|
+
(getVariableByName('checkboxItem/padding/vertical', modes) as number | null) ?? 0
|
|
136
|
+
|
|
137
|
+
const labelColor =
|
|
138
|
+
(getVariableByName('checkboxItem/foreground', modes) as string | null) ?? '#1a1c1f'
|
|
139
|
+
const labelFontFamily =
|
|
140
|
+
(getVariableByName('checkboxItem/label/fontFamily', modes) as string | null) ?? 'JioType Var'
|
|
141
|
+
const labelFontSize =
|
|
142
|
+
(getVariableByName('checkboxItem/label/fontSize', modes) as number | null) ?? 14
|
|
143
|
+
const labelLineHeight =
|
|
144
|
+
(getVariableByName('checkboxItem/label/lineHeight', modes) as number | null) ?? 19
|
|
145
|
+
const labelFontWeightRaw =
|
|
146
|
+
getVariableByName('checkboxItem/label/fontWeight', modes) ?? 400
|
|
147
|
+
const labelFontWeight = String(labelFontWeightRaw) as TextStyle['fontWeight']
|
|
148
|
+
|
|
149
|
+
const containerStyle: ViewStyle = useMemo(
|
|
150
|
+
() => ({
|
|
151
|
+
flexDirection: isExpanded ? 'column' : 'row',
|
|
152
|
+
alignItems: isExpanded ? 'flex-end' : 'center',
|
|
153
|
+
gap,
|
|
154
|
+
width: '100%',
|
|
155
|
+
...(disabled ? { opacity: 0.6 } : null),
|
|
156
|
+
}),
|
|
157
|
+
[isExpanded, gap, disabled]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const rowStyle: ViewStyle = useMemo(
|
|
161
|
+
() => ({
|
|
162
|
+
flex: isExpanded ? undefined : 1,
|
|
163
|
+
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
164
|
+
minWidth: 0,
|
|
165
|
+
flexDirection: 'row',
|
|
166
|
+
alignItems: 'flex-start',
|
|
167
|
+
gap: rowGap,
|
|
168
|
+
paddingHorizontal: rowPaddingHorizontal,
|
|
169
|
+
paddingVertical: rowPaddingVertical,
|
|
170
|
+
}),
|
|
171
|
+
[isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
const resolvedLabelStyle: TextStyle = useMemo(
|
|
175
|
+
() => ({
|
|
176
|
+
flex: 1,
|
|
177
|
+
minWidth: 0,
|
|
178
|
+
color: labelColor,
|
|
179
|
+
fontFamily: labelFontFamily,
|
|
180
|
+
fontSize: labelFontSize,
|
|
181
|
+
lineHeight: labelLineHeight,
|
|
182
|
+
fontWeight: labelFontWeight,
|
|
183
|
+
}),
|
|
184
|
+
[
|
|
185
|
+
labelColor,
|
|
186
|
+
labelFontFamily,
|
|
187
|
+
labelFontSize,
|
|
188
|
+
labelLineHeight,
|
|
189
|
+
labelFontWeight,
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
const buttonModes = useMemo(
|
|
194
|
+
() => ({ ...BUTTON_DEFAULT_MODES, ...modes }),
|
|
195
|
+
[modes]
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const a11yLabel =
|
|
199
|
+
accessibilityLabel ?? (typeof label === 'string' ? label : undefined)
|
|
200
|
+
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel
|
|
201
|
+
|
|
202
|
+
const labelNumberOfLinesProps =
|
|
203
|
+
!isExpanded && collapsedLines > 0
|
|
204
|
+
? { numberOfLines: collapsedLines, ellipsizeMode: 'tail' as const }
|
|
205
|
+
: null
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<View style={[containerStyle, style]}>
|
|
209
|
+
<View style={rowStyle}>
|
|
210
|
+
<Checkbox
|
|
211
|
+
checked={isChecked}
|
|
212
|
+
disabled={disabled}
|
|
213
|
+
onValueChange={handleToggleChecked}
|
|
214
|
+
modes={modes}
|
|
215
|
+
{...(a11yLabel !== undefined ? { accessibilityLabel: a11yLabel } : {})}
|
|
216
|
+
/>
|
|
217
|
+
<Text
|
|
218
|
+
style={[resolvedLabelStyle, labelStyle]}
|
|
219
|
+
selectable={false}
|
|
220
|
+
{...(labelNumberOfLinesProps ?? {})}
|
|
221
|
+
>
|
|
222
|
+
{label}
|
|
223
|
+
</Text>
|
|
224
|
+
</View>
|
|
225
|
+
<Button
|
|
226
|
+
label={buttonLabel}
|
|
227
|
+
onPress={handleToggleExpanded}
|
|
228
|
+
disabled={disabled}
|
|
229
|
+
modes={buttonModes}
|
|
230
|
+
accessibilityLabel={buttonLabel}
|
|
231
|
+
accessibilityState={{ expanded: isExpanded }}
|
|
232
|
+
/>
|
|
233
|
+
</View>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default ExpandableCheckbox
|