jfs-components 0.0.73 → 0.0.77
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 +115 -6
- package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
- package/lib/commonjs/components/AppBar/AppBar.js +17 -11
- 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/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/IconButton/IconButton.js +20 -0
- package/lib/commonjs/components/Image/Image.js +26 -1
- package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
- 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/PageHero/PageHero.js +189 -0
- package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
- package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
- package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
- package/lib/commonjs/components/Text/Text.js +40 -3
- package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
- package/lib/commonjs/components/index.js +67 -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/AccountCard/AccountCard.js +241 -0
- package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
- package/lib/module/components/AppBar/AppBar.js +17 -11
- 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/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/IconButton/IconButton.js +20 -0
- package/lib/module/components/Image/Image.js +25 -1
- package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
- 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/PageHero/PageHero.js +183 -0
- package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
- package/lib/module/components/PoweredByLabel/finvu.png +0 -0
- package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
- package/lib/module/components/Text/Text.js +40 -3
- package/lib/module/components/Tooltip/Tooltip.js +34 -27
- package/lib/module/components/index.js +8 -1
- 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/AccountCard/AccountCard.d.ts +81 -0
- 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/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/IconButton/IconButton.d.ts +7 -1
- package/lib/typescript/src/components/Image/Image.d.ts +8 -1
- package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
- 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/PageHero/PageHero.d.ts +79 -0
- package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
- package/lib/typescript/src/components/Text/Text.d.ts +31 -2
- package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
- package/lib/typescript/src/components/index.d.ts +8 -1
- 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 -3
- package/src/components/AccountCard/AccountCard.tsx +376 -0
- package/src/components/ActionFooter/ActionFooter.tsx +152 -86
- package/src/components/AppBar/AppBar.tsx +25 -14
- 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/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/IconButton/IconButton.tsx +27 -0
- package/src/components/Image/Image.tsx +25 -0
- package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
- 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/PageHero/PageHero.tsx +257 -0
- package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
- package/src/components/PoweredByLabel/finvu.png +0 -0
- package/src/components/RechargeCard/RechargeCard.tsx +32 -24
- package/src/components/Text/Text.tsx +78 -3
- package/src/components/Tooltip/Tooltip.tsx +50 -25
- package/src/components/index.ts +16 -1
- 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
|
@@ -12,6 +12,8 @@ import Icon from '../../icons/Icon'
|
|
|
12
12
|
import { usePressableWebSupport, type SafePressableProps, type WebAccessibilityProps } from '../../utils/web-platform-utils'
|
|
13
13
|
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
14
14
|
import type { UnifiedSource } from '../../utils/MediaSource'
|
|
15
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
16
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
15
17
|
|
|
16
18
|
type IconButtonProps = SafePressableProps & {
|
|
17
19
|
/** Built-in icon name from the registry (default state). */
|
|
@@ -65,6 +67,12 @@ type IconButtonProps = SafePressableProps & {
|
|
|
65
67
|
* Web-specific accessibility props (only used on web platform)
|
|
66
68
|
*/
|
|
67
69
|
webAccessibilityProps?: WebAccessibilityProps;
|
|
70
|
+
/**
|
|
71
|
+
* Explicit per-instance loading override. When `true`, renders a
|
|
72
|
+
* same-size pill-shaped skeleton instead of the button. Defaults to
|
|
73
|
+
* inheriting from the surrounding `<SkeletonGroup>`.
|
|
74
|
+
*/
|
|
75
|
+
loading?: boolean;
|
|
68
76
|
};
|
|
69
77
|
|
|
70
78
|
// ---------------------------------------------------------------------------
|
|
@@ -155,6 +163,7 @@ function IconButton({
|
|
|
155
163
|
inactiveIcon,
|
|
156
164
|
inactiveSource,
|
|
157
165
|
isActive = false,
|
|
166
|
+
loading,
|
|
158
167
|
...rest
|
|
159
168
|
}: IconButtonProps) {
|
|
160
169
|
// Merge explicit props with modes for token resolution. Memoize the merged
|
|
@@ -174,6 +183,11 @@ function IconButton({
|
|
|
174
183
|
[componentModes, disabled]
|
|
175
184
|
)
|
|
176
185
|
|
|
186
|
+
// Hook called unconditionally — short-circuit below comes AFTER all hooks
|
|
187
|
+
// to keep React's hook order stable across renders.
|
|
188
|
+
const { active: groupActive } = useSkeleton()
|
|
189
|
+
const isLoading = loading ?? groupActive
|
|
190
|
+
|
|
177
191
|
const [isFocused, setIsFocused] = useState(false)
|
|
178
192
|
const [isHovered, setIsHovered] = useState(false)
|
|
179
193
|
|
|
@@ -271,6 +285,19 @@ function IconButton({
|
|
|
271
285
|
[tokens.baseContainerStyle, style, isHovered, isFocused, disabled]
|
|
272
286
|
)
|
|
273
287
|
|
|
288
|
+
if (isLoading) {
|
|
289
|
+
const size = tokens.baseContainerStyle.width as number
|
|
290
|
+
return (
|
|
291
|
+
<Skeleton
|
|
292
|
+
kind="other"
|
|
293
|
+
width={size}
|
|
294
|
+
height={size}
|
|
295
|
+
style={style as any}
|
|
296
|
+
modes={componentModes}
|
|
297
|
+
/>
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
274
301
|
return (
|
|
275
302
|
<Pressable
|
|
276
303
|
accessibilityRole="button"
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
type ViewStyle,
|
|
9
9
|
type ImageResizeMode,
|
|
10
10
|
} from 'react-native'
|
|
11
|
+
import Skeleton from '../../skeleton/Skeleton'
|
|
12
|
+
import { useSkeleton } from '../../skeleton/SkeletonGroup'
|
|
11
13
|
|
|
12
14
|
export type ImageProps = {
|
|
13
15
|
/**
|
|
@@ -52,6 +54,13 @@ export type ImageProps = {
|
|
|
52
54
|
| 'no'
|
|
53
55
|
| 'no-hide-descendants'
|
|
54
56
|
| undefined
|
|
57
|
+
/**
|
|
58
|
+
* Explicit per-instance loading override. When `true`, the image renders as
|
|
59
|
+
* a skeleton placeholder at the same box size; when `false`, the
|
|
60
|
+
* surrounding `<SkeletonGroup>` is ignored. Defaults to inheriting from
|
|
61
|
+
* the group.
|
|
62
|
+
*/
|
|
63
|
+
loading?: boolean | undefined
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
function normalizeSource(
|
|
@@ -94,6 +103,7 @@ function Image({
|
|
|
94
103
|
accessibilityLabel,
|
|
95
104
|
accessibilityElementsHidden,
|
|
96
105
|
importantForAccessibility,
|
|
106
|
+
loading,
|
|
97
107
|
}: ImageProps) {
|
|
98
108
|
const source = useMemo(() => normalizeSource(imageSource), [imageSource])
|
|
99
109
|
|
|
@@ -112,6 +122,21 @@ function Image({
|
|
|
112
122
|
return s
|
|
113
123
|
}, [ratio, width, height, borderRadius])
|
|
114
124
|
|
|
125
|
+
const { active: groupActive } = useSkeleton()
|
|
126
|
+
const isLoading = loading ?? groupActive
|
|
127
|
+
if (isLoading) {
|
|
128
|
+
// Match the loaded image's exact box. If height is unknown but a ratio
|
|
129
|
+
// is set, the skeleton uses `aspectRatio` the same way the loaded image
|
|
130
|
+
// would, so layout never jumps when the load resolves.
|
|
131
|
+
const skeletonStyle: ViewStyle = {
|
|
132
|
+
width: (width ?? '100%') as ViewStyle['width'],
|
|
133
|
+
...(height != null
|
|
134
|
+
? { height: height as number }
|
|
135
|
+
: { aspectRatio: ratio }),
|
|
136
|
+
}
|
|
137
|
+
return <Skeleton kind="image" style={skeletonStyle} />
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
if (!source) {
|
|
116
141
|
return <View style={[layoutStyle, style as StyleProp<ViewStyle>]} />
|
|
117
142
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { useMemo } 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 { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
11
|
+
import Button from '../Button/Button'
|
|
12
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MEDIA_SIZE = 117
|
|
15
|
+
|
|
16
|
+
export type LottieIntroBlockProps = {
|
|
17
|
+
/** Headline text shown below the media area. */
|
|
18
|
+
title?: string
|
|
19
|
+
/** Whether to render the supportive paragraph below the title. */
|
|
20
|
+
showSupportText?: boolean
|
|
21
|
+
/** Body/supportive text shown below the title. */
|
|
22
|
+
supportText?: string
|
|
23
|
+
/** Whether to render the action button at the bottom. */
|
|
24
|
+
showButton?: boolean
|
|
25
|
+
/** Label for the default action button. Ignored when `buttonSlot` is provided. */
|
|
26
|
+
buttonLabel?: string
|
|
27
|
+
/** Press handler for the default action button. Ignored when `buttonSlot` is provided. */
|
|
28
|
+
onButtonPress?: () => void
|
|
29
|
+
/**
|
|
30
|
+
* Custom slot for the media area (Lottie animation, illustration, or image).
|
|
31
|
+
* Should render at the design size of 117x117. If omitted, a neutral
|
|
32
|
+
* placeholder of the same size is rendered so the layout stays stable.
|
|
33
|
+
* `modes` are automatically cascaded into this slot.
|
|
34
|
+
*/
|
|
35
|
+
media?: React.ReactNode
|
|
36
|
+
/**
|
|
37
|
+
* Optional slot to fully override the action button.
|
|
38
|
+
* When provided, `showButton`, `buttonLabel`, and `onButtonPress` are ignored.
|
|
39
|
+
* `modes` are automatically cascaded into this slot.
|
|
40
|
+
*/
|
|
41
|
+
buttonSlot?: React.ReactNode
|
|
42
|
+
/** Mode configuration for design-token theming. */
|
|
43
|
+
modes?: Record<string, any>
|
|
44
|
+
/** Style overrides applied to the outer container. */
|
|
45
|
+
style?: StyleProp<ViewStyle>
|
|
46
|
+
testID?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* LottieIntroBlock displays a centered onboarding/intro block composed of a
|
|
51
|
+
* media slot (typically a Lottie animation or illustration) above a title,
|
|
52
|
+
* an optional supportive paragraph, and an optional action button.
|
|
53
|
+
*
|
|
54
|
+
* All visual values are resolved from Figma design tokens via
|
|
55
|
+
* `getVariableByName`. Slots cascade the active `modes` to their children
|
|
56
|
+
* through `cloneChildrenWithModes`.
|
|
57
|
+
*
|
|
58
|
+
* @component
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* <LottieIntroBlock
|
|
62
|
+
* title="Let's get to know how your financial health is doing"
|
|
63
|
+
* supportText="From assets to taxes, stay on top of everything in one simple view."
|
|
64
|
+
* buttonLabel="Get started"
|
|
65
|
+
* onButtonPress={() => navigate('NextScreen')}
|
|
66
|
+
* media={<MyLottiePlayer source={animationSource} />}
|
|
67
|
+
* />
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function LottieIntroBlock({
|
|
71
|
+
title = "Let's get to know how your financial health is doing",
|
|
72
|
+
showSupportText = true,
|
|
73
|
+
supportText = 'From assets to taxes, stay on top of everything in one simple view.',
|
|
74
|
+
showButton = true,
|
|
75
|
+
buttonLabel = 'Button',
|
|
76
|
+
onButtonPress,
|
|
77
|
+
media,
|
|
78
|
+
buttonSlot,
|
|
79
|
+
modes: propModes = EMPTY_MODES,
|
|
80
|
+
style,
|
|
81
|
+
testID,
|
|
82
|
+
}: LottieIntroBlockProps) {
|
|
83
|
+
const { modes: globalModes } = useTokens()
|
|
84
|
+
const modes = useMemo(
|
|
85
|
+
() => ({ ...globalModes, ...propModes }),
|
|
86
|
+
[globalModes, propModes]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Container
|
|
90
|
+
const gap = Number(getVariableByName('lottieIntroBlock/gap', modes)) || 36
|
|
91
|
+
const paddingHorizontal =
|
|
92
|
+
Number(getVariableByName('lottieIntroBlock/padding/horizontal', modes)) || 0
|
|
93
|
+
const paddingVertical =
|
|
94
|
+
Number(getVariableByName('lottieIntroBlock/padding/vertical', modes)) || 16
|
|
95
|
+
|
|
96
|
+
// Text wrap
|
|
97
|
+
const textWrapGap =
|
|
98
|
+
Number(getVariableByName('lottieIntroBlock/textWrap/gap', modes)) || 16
|
|
99
|
+
|
|
100
|
+
// Title
|
|
101
|
+
const titleColor =
|
|
102
|
+
getVariableByName('lottieIntroBlock/title/foreground', modes) || '#0d0d0f'
|
|
103
|
+
const titleFontSize =
|
|
104
|
+
Number(getVariableByName('lottieIntroBlock/title/fontSize', modes)) || 23
|
|
105
|
+
const titleFontFamily =
|
|
106
|
+
getVariableByName('lottieIntroBlock/title/fontFamily', modes) || 'System'
|
|
107
|
+
const titleLineHeight =
|
|
108
|
+
Number(getVariableByName('lottieIntroBlock/title/lineHeight', modes)) || 23
|
|
109
|
+
const titleFontWeight =
|
|
110
|
+
getVariableByName('lottieIntroBlock/title/fontWeight', modes) || 900
|
|
111
|
+
|
|
112
|
+
// Support text
|
|
113
|
+
const supportColor =
|
|
114
|
+
getVariableByName('lottieIntroBlock/supportText/foreground', modes) ||
|
|
115
|
+
'#0d0d0f'
|
|
116
|
+
const supportFontSize =
|
|
117
|
+
Number(getVariableByName('lottieIntroBlock/supportText/fontSize', modes)) ||
|
|
118
|
+
14
|
|
119
|
+
const supportFontFamily =
|
|
120
|
+
getVariableByName('lottieIntroBlock/supportText/fontFamily', modes) ||
|
|
121
|
+
'System'
|
|
122
|
+
const supportLineHeight =
|
|
123
|
+
Number(
|
|
124
|
+
getVariableByName('lottieIntroBlock/supportText/lineHeight', modes)
|
|
125
|
+
) || 18
|
|
126
|
+
const supportFontWeight =
|
|
127
|
+
getVariableByName('lottieIntroBlock/supportText/fontWeight', modes) || 400
|
|
128
|
+
|
|
129
|
+
const containerStyle: ViewStyle = {
|
|
130
|
+
flexDirection: 'column',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
paddingHorizontal,
|
|
133
|
+
paddingVertical,
|
|
134
|
+
gap,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const textWrapStyle: ViewStyle = {
|
|
138
|
+
flexDirection: 'column',
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
gap: textWrapGap,
|
|
141
|
+
width: '100%',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const titleStyle: TextStyle = {
|
|
145
|
+
color: titleColor,
|
|
146
|
+
fontSize: titleFontSize,
|
|
147
|
+
fontFamily: titleFontFamily,
|
|
148
|
+
lineHeight: titleLineHeight,
|
|
149
|
+
fontWeight: String(titleFontWeight) as TextStyle['fontWeight'],
|
|
150
|
+
textAlign: 'center',
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const supportTextStyle: TextStyle = {
|
|
154
|
+
color: supportColor,
|
|
155
|
+
fontSize: supportFontSize,
|
|
156
|
+
fontFamily: supportFontFamily,
|
|
157
|
+
lineHeight: supportLineHeight,
|
|
158
|
+
fontWeight: String(supportFontWeight) as TextStyle['fontWeight'],
|
|
159
|
+
textAlign: 'center',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const mediaContent = useMemo(() => {
|
|
163
|
+
if (media === undefined || media === null) {
|
|
164
|
+
return (
|
|
165
|
+
<View
|
|
166
|
+
style={{
|
|
167
|
+
width: DEFAULT_MEDIA_SIZE,
|
|
168
|
+
height: DEFAULT_MEDIA_SIZE,
|
|
169
|
+
}}
|
|
170
|
+
accessibilityElementsHidden
|
|
171
|
+
importantForAccessibility="no-hide-descendants"
|
|
172
|
+
/>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
return cloneChildrenWithModes(media, modes)
|
|
176
|
+
}, [media, modes])
|
|
177
|
+
|
|
178
|
+
const buttonContent = useMemo(() => {
|
|
179
|
+
if (buttonSlot !== undefined && buttonSlot !== null) {
|
|
180
|
+
return cloneChildrenWithModes(buttonSlot, modes)
|
|
181
|
+
}
|
|
182
|
+
if (!showButton) {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
return <Button label={buttonLabel} onPress={onButtonPress} modes={modes} />
|
|
186
|
+
}, [buttonSlot, showButton, buttonLabel, onButtonPress, modes])
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<View style={[containerStyle, style]} testID={testID}>
|
|
190
|
+
{mediaContent}
|
|
191
|
+
<View style={textWrapStyle}>
|
|
192
|
+
<Text style={titleStyle}>{title}</Text>
|
|
193
|
+
{showSupportText && supportText ? (
|
|
194
|
+
<Text style={supportTextStyle}>{supportText}</Text>
|
|
195
|
+
) : null}
|
|
196
|
+
{buttonContent}
|
|
197
|
+
</View>
|
|
198
|
+
</View>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default LottieIntroBlock
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
6
|
+
import { getNativeLottieView } from './loadNativeLottieView'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A parsed Lottie animation. The JSON object you get from
|
|
10
|
+
* `require('./animation.json')` or `fetch().then(r => r.json())`. We keep the
|
|
11
|
+
* type intentionally loose because both `lottie-react-native` and `lottie-react`
|
|
12
|
+
* accept slightly different shapes — `LottiePlayer` narrows back to the
|
|
13
|
+
* platform-specific type internally.
|
|
14
|
+
*/
|
|
15
|
+
export type LottieAnimationSource = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
export type LottiePlayerProps = {
|
|
18
|
+
/**
|
|
19
|
+
* Parsed Lottie animation JSON. Use `require('./animation.json')` in React
|
|
20
|
+
* Native or `import animation from './animation.json'` on web.
|
|
21
|
+
*
|
|
22
|
+
* URI sources (`{ uri: '...' }`) are intentionally not accepted here — web
|
|
23
|
+
* Lottie players require the animation data to be pre-parsed. Fetch and
|
|
24
|
+
* parse the JSON yourself before passing it in if you need a remote source.
|
|
25
|
+
*/
|
|
26
|
+
source: LottieAnimationSource
|
|
27
|
+
/**
|
|
28
|
+
* Override the rendered size. Pass a number for a square box, or
|
|
29
|
+
* `{ width, height }` for non-square.
|
|
30
|
+
*
|
|
31
|
+
* When omitted, size is resolved from the `media/width` and `media/height`
|
|
32
|
+
* design tokens (default `117 × 117`). The `Media / Output` collection
|
|
33
|
+
* exposes `L | M | S` modes (117 / 70 / 20) — pass
|
|
34
|
+
* `modes={{ 'Media / Output': 'M' }}` to render at 70×70, etc.
|
|
35
|
+
*/
|
|
36
|
+
size?: number | { width: number; height: number }
|
|
37
|
+
/** Play the animation on mount. Defaults to `true`. */
|
|
38
|
+
autoPlay?: boolean
|
|
39
|
+
/** Loop the animation. Defaults to `true`. */
|
|
40
|
+
loop?: boolean
|
|
41
|
+
/** Mode configuration for design-token theming. */
|
|
42
|
+
modes?: Record<string, any>
|
|
43
|
+
/** Style overrides applied to the underlying view. */
|
|
44
|
+
style?: StyleProp<ViewStyle>
|
|
45
|
+
/** Accessibility label. Lottie is decorative by default. */
|
|
46
|
+
accessibilityLabel?: string
|
|
47
|
+
testID?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_SIZE = 117
|
|
51
|
+
|
|
52
|
+
function resolveSize(
|
|
53
|
+
size: LottiePlayerProps['size'],
|
|
54
|
+
modes: Record<string, any>
|
|
55
|
+
) {
|
|
56
|
+
if (typeof size === 'number') return { width: size, height: size }
|
|
57
|
+
if (size && typeof size === 'object') return size
|
|
58
|
+
const width =
|
|
59
|
+
Number(getVariableByName('media/width', modes)) || DEFAULT_SIZE
|
|
60
|
+
const height =
|
|
61
|
+
Number(getVariableByName('media/height', modes)) || DEFAULT_SIZE
|
|
62
|
+
return { width, height }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Renders a Lottie animation using the consumer's installed
|
|
67
|
+
* `lottie-react-native` (native) or `lottie-react` (web) — both are declared
|
|
68
|
+
* as **optional peer dependencies** of `jfs-components`, so installing the
|
|
69
|
+
* library does not pull them in. Add the relevant package to your app only
|
|
70
|
+
* if you actually use `LottiePlayer`:
|
|
71
|
+
*
|
|
72
|
+
* ```sh
|
|
73
|
+
* # React Native (iOS / Android)
|
|
74
|
+
* npm install lottie-react-native
|
|
75
|
+
* cd ios && pod install
|
|
76
|
+
*
|
|
77
|
+
* # Web (or react-native-web)
|
|
78
|
+
* npm install lottie-react
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* The web build (`LottiePlayer.web.tsx`) is picked automatically by Metro /
|
|
82
|
+
* webpack via platform extensions — same pattern as `MediaCard/GlassFill`.
|
|
83
|
+
*
|
|
84
|
+
* Token-driven sizing: when `size` is omitted, `LottiePlayer` reads
|
|
85
|
+
* `media/width` and `media/height` from the Figma variables resolver, so the
|
|
86
|
+
* animation matches the surrounding component's `Media / Output` mode
|
|
87
|
+
* automatically. This is the same sizing contract `PageHero` and
|
|
88
|
+
* `LottieIntroBlock` use for their `media` slots.
|
|
89
|
+
*
|
|
90
|
+
* @component
|
|
91
|
+
* @example
|
|
92
|
+
* ```tsx
|
|
93
|
+
* import animation from './assets/loader.json';
|
|
94
|
+
*
|
|
95
|
+
* <LottiePlayer source={animation} /> // 117 × 117 (default)
|
|
96
|
+
* <LottiePlayer source={animation} size={70} /> // 70 × 70
|
|
97
|
+
* <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20 × 20
|
|
98
|
+
* <PageHero media={<LottiePlayer source={animation} />} />
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
function LottiePlayer({
|
|
102
|
+
source,
|
|
103
|
+
size,
|
|
104
|
+
autoPlay = true,
|
|
105
|
+
loop = true,
|
|
106
|
+
modes: propModes = EMPTY_MODES,
|
|
107
|
+
style,
|
|
108
|
+
accessibilityLabel,
|
|
109
|
+
testID,
|
|
110
|
+
}: LottiePlayerProps) {
|
|
111
|
+
const { modes: globalModes } = useTokens()
|
|
112
|
+
const modes = useMemo(
|
|
113
|
+
() =>
|
|
114
|
+
globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
115
|
+
? EMPTY_MODES
|
|
116
|
+
: { ...globalModes, ...propModes },
|
|
117
|
+
[globalModes, propModes]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const { width, height } = useMemo(
|
|
121
|
+
() => resolveSize(size, modes),
|
|
122
|
+
[size, modes]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const NativeLottieView = useMemo(() => getNativeLottieView(), [])
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View
|
|
129
|
+
style={[{ width, height }, style]}
|
|
130
|
+
testID={testID}
|
|
131
|
+
accessibilityLabel={accessibilityLabel}
|
|
132
|
+
accessibilityElementsHidden={accessibilityLabel ? undefined : true}
|
|
133
|
+
importantForAccessibility={accessibilityLabel ? 'auto' : 'no'}
|
|
134
|
+
>
|
|
135
|
+
<NativeLottieView
|
|
136
|
+
source={source}
|
|
137
|
+
autoPlay={autoPlay}
|
|
138
|
+
loop={loop}
|
|
139
|
+
style={{ width: '100%', height: '100%' }}
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default React.memo(LottiePlayer)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native'
|
|
3
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
4
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
5
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
6
|
+
import { getWebLottieView } from './loadWebLottieView'
|
|
7
|
+
|
|
8
|
+
export type LottieAnimationSource = Record<string, unknown>
|
|
9
|
+
|
|
10
|
+
export type LottiePlayerProps = {
|
|
11
|
+
source: LottieAnimationSource
|
|
12
|
+
size?: number | { width: number; height: number }
|
|
13
|
+
autoPlay?: boolean
|
|
14
|
+
loop?: boolean
|
|
15
|
+
modes?: Record<string, any>
|
|
16
|
+
style?: StyleProp<ViewStyle>
|
|
17
|
+
accessibilityLabel?: string
|
|
18
|
+
testID?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SIZE = 117
|
|
22
|
+
|
|
23
|
+
function resolveSize(
|
|
24
|
+
size: LottiePlayerProps['size'],
|
|
25
|
+
modes: Record<string, any>
|
|
26
|
+
) {
|
|
27
|
+
if (typeof size === 'number') return { width: size, height: size }
|
|
28
|
+
if (size && typeof size === 'object') return size
|
|
29
|
+
const width =
|
|
30
|
+
Number(getVariableByName('media/width', modes)) || DEFAULT_SIZE
|
|
31
|
+
const height =
|
|
32
|
+
Number(getVariableByName('media/height', modes)) || DEFAULT_SIZE
|
|
33
|
+
return { width, height }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Web build of `LottiePlayer` — picked automatically by webpack /
|
|
38
|
+
* Metro-for-web via the `.web.tsx` platform extension. Uses `lottie-react`
|
|
39
|
+
* (which wraps `lottie-web`) and renders a plain DOM container.
|
|
40
|
+
*
|
|
41
|
+
* Public API mirrors `LottiePlayer.tsx` (native). See that file for the
|
|
42
|
+
* documented prop reference and usage patterns.
|
|
43
|
+
*/
|
|
44
|
+
function LottiePlayer({
|
|
45
|
+
source,
|
|
46
|
+
size,
|
|
47
|
+
autoPlay = true,
|
|
48
|
+
loop = true,
|
|
49
|
+
modes: propModes = EMPTY_MODES,
|
|
50
|
+
style,
|
|
51
|
+
accessibilityLabel,
|
|
52
|
+
testID,
|
|
53
|
+
}: LottiePlayerProps) {
|
|
54
|
+
const { modes: globalModes } = useTokens()
|
|
55
|
+
const modes = useMemo(
|
|
56
|
+
() =>
|
|
57
|
+
globalModes === EMPTY_MODES && propModes === EMPTY_MODES
|
|
58
|
+
? EMPTY_MODES
|
|
59
|
+
: { ...globalModes, ...propModes },
|
|
60
|
+
[globalModes, propModes]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const { width, height } = useMemo(
|
|
64
|
+
() => resolveSize(size, modes),
|
|
65
|
+
[size, modes]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const WebLottieView = useMemo(() => getWebLottieView(), [])
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
style={{
|
|
73
|
+
width,
|
|
74
|
+
height,
|
|
75
|
+
display: 'flex',
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
...(style as React.CSSProperties),
|
|
79
|
+
}}
|
|
80
|
+
data-testid={testID}
|
|
81
|
+
aria-label={accessibilityLabel}
|
|
82
|
+
aria-hidden={accessibilityLabel ? undefined : true}
|
|
83
|
+
>
|
|
84
|
+
<WebLottieView
|
|
85
|
+
animationData={source}
|
|
86
|
+
autoplay={autoPlay}
|
|
87
|
+
loop={loop}
|
|
88
|
+
style={{ width: '100%', height: '100%' }}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default React.memo(LottiePlayer)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text, View, type ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
/** Props we forward to the underlying native Lottie view. */
|
|
5
|
+
export type NativeLottieViewProps = {
|
|
6
|
+
source: Record<string, unknown>
|
|
7
|
+
autoPlay?: boolean
|
|
8
|
+
loop?: boolean
|
|
9
|
+
style?: ViewStyle
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const INSTALL_HINT =
|
|
13
|
+
'LottiePlayer requires lottie-react-native in your app.\n' +
|
|
14
|
+
' npm install lottie-react-native\n' +
|
|
15
|
+
' cd ios && pod install'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Metro resolves `require('lottie-react-native')` at bundle time even inside
|
|
19
|
+
* try/catch, which breaks apps that import `jfs-components` without having
|
|
20
|
+
* the optional peer installed. Splitting the module id into a runtime string
|
|
21
|
+
* keeps Metro from statically linking it — the native module is loaded only
|
|
22
|
+
* when present in the consumer's node_modules.
|
|
23
|
+
*/
|
|
24
|
+
function resolveNativeLottieModuleName() {
|
|
25
|
+
return ['lottie', '-react', '-native'].join('')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function LottieUnavailableView({ style }: Pick<NativeLottieViewProps, 'style'>) {
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
return (
|
|
31
|
+
<View
|
|
32
|
+
style={[
|
|
33
|
+
style,
|
|
34
|
+
{
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
backgroundColor: 'rgba(255, 196, 0, 0.12)',
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: 'rgba(255, 196, 0, 0.45)',
|
|
40
|
+
borderRadius: 8,
|
|
41
|
+
padding: 8,
|
|
42
|
+
},
|
|
43
|
+
]}
|
|
44
|
+
>
|
|
45
|
+
<Text
|
|
46
|
+
style={{
|
|
47
|
+
color: '#8a6d00',
|
|
48
|
+
fontSize: 11,
|
|
49
|
+
textAlign: 'center',
|
|
50
|
+
lineHeight: 15,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
{INSTALL_HINT}
|
|
54
|
+
</Text>
|
|
55
|
+
</View>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return <View style={style} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function LottieUnavailable(props: NativeLottieViewProps) {
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (__DEV__) {
|
|
65
|
+
console.warn(`[jfs-components/LottiePlayer] ${INSTALL_HINT}`)
|
|
66
|
+
}
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
return <LottieUnavailableView style={props.style} />
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let cachedView: React.ComponentType<NativeLottieViewProps> | undefined
|
|
73
|
+
|
|
74
|
+
export function getNativeLottieView(): React.ComponentType<NativeLottieViewProps> {
|
|
75
|
+
if (cachedView !== undefined) return cachedView
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const mod = require(resolveNativeLottieModuleName()) as {
|
|
79
|
+
default?: React.ComponentType<NativeLottieViewProps>
|
|
80
|
+
}
|
|
81
|
+
cachedView = mod.default ?? LottieUnavailable
|
|
82
|
+
} catch {
|
|
83
|
+
cachedView = LottieUnavailable
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return cachedView
|
|
87
|
+
}
|