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.
Files changed (134) hide show
  1. package/CHANGELOG.md +115 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  6. package/lib/commonjs/components/Badge/Badge.js +23 -0
  7. package/lib/commonjs/components/Button/Button.js +37 -0
  8. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  10. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  12. package/lib/commonjs/components/FormField/FormField.js +328 -178
  13. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  14. package/lib/commonjs/components/Image/Image.js +26 -1
  15. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  17. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  18. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  19. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  20. package/lib/commonjs/components/PageHero/PageHero.js +189 -0
  21. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  22. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  23. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  24. package/lib/commonjs/components/Text/Text.js +40 -3
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  26. package/lib/commonjs/components/index.js +67 -0
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/Icon.js +16 -0
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/commonjs/index.js +12 -0
  31. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  32. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  33. package/lib/commonjs/skeleton/index.js +58 -0
  34. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  35. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  36. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  37. package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
  38. package/lib/module/components/AppBar/AppBar.js +17 -11
  39. package/lib/module/components/Avatar/Avatar.js +19 -0
  40. package/lib/module/components/Badge/Badge.js +23 -0
  41. package/lib/module/components/Button/Button.js +37 -0
  42. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  43. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  44. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  45. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  46. package/lib/module/components/FormField/FormField.js +330 -180
  47. package/lib/module/components/IconButton/IconButton.js +20 -0
  48. package/lib/module/components/Image/Image.js +25 -1
  49. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  50. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  51. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  52. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  53. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  54. package/lib/module/components/PageHero/PageHero.js +183 -0
  55. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  56. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  57. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  58. package/lib/module/components/Text/Text.js +40 -3
  59. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  60. package/lib/module/components/index.js +8 -1
  61. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  62. package/lib/module/icons/Icon.js +16 -0
  63. package/lib/module/icons/registry.js +1 -1
  64. package/lib/module/index.js +2 -1
  65. package/lib/module/skeleton/Skeleton.js +229 -0
  66. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  67. package/lib/module/skeleton/index.js +6 -0
  68. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  69. package/lib/module/skeleton/useReducedMotion.js +61 -0
  70. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  71. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  72. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  73. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  74. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  75. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  76. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  77. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  78. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  79. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  80. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  81. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  82. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  83. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  84. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  85. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  86. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  87. package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
  88. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  89. package/lib/typescript/src/components/Text/Text.d.ts +31 -2
  90. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  91. package/lib/typescript/src/components/index.d.ts +8 -1
  92. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  93. package/lib/typescript/src/icons/registry.d.ts +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -0
  95. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  96. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  97. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  98. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  99. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  100. package/package.json +11 -3
  101. package/src/components/AccountCard/AccountCard.tsx +376 -0
  102. package/src/components/ActionFooter/ActionFooter.tsx +152 -86
  103. package/src/components/AppBar/AppBar.tsx +25 -14
  104. package/src/components/Avatar/Avatar.tsx +26 -0
  105. package/src/components/Badge/Badge.tsx +27 -0
  106. package/src/components/Button/Button.tsx +40 -0
  107. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  108. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  109. package/src/components/Dropdown/Dropdown.tsx +331 -0
  110. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  111. package/src/components/FormField/FormField.tsx +542 -215
  112. package/src/components/IconButton/IconButton.tsx +27 -0
  113. package/src/components/Image/Image.tsx +25 -0
  114. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  115. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  116. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  117. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  118. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  119. package/src/components/PageHero/PageHero.tsx +257 -0
  120. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  121. package/src/components/PoweredByLabel/finvu.png +0 -0
  122. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  123. package/src/components/Text/Text.tsx +78 -3
  124. package/src/components/Tooltip/Tooltip.tsx +50 -25
  125. package/src/components/index.ts +16 -1
  126. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  127. package/src/icons/Icon.tsx +17 -0
  128. package/src/icons/registry.ts +1 -1
  129. package/src/index.ts +1 -0
  130. package/src/skeleton/Skeleton.tsx +298 -0
  131. package/src/skeleton/SkeletonGroup.tsx +193 -0
  132. package/src/skeleton/index.ts +10 -0
  133. package/src/skeleton/shimmer-tokens.ts +221 -0
  134. 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
+ }