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.
Files changed (146) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
  4. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  5. package/lib/commonjs/components/Badge/Badge.js +23 -0
  6. package/lib/commonjs/components/Button/Button.js +37 -0
  7. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  8. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  9. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  10. package/lib/commonjs/components/FormField/FormField.js +14 -1
  11. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  12. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  13. package/lib/commonjs/components/Image/Image.js +26 -1
  14. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  15. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  17. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  18. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  19. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  20. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  21. package/lib/commonjs/components/PageHero/PageHero.js +41 -5
  22. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  23. package/lib/commonjs/components/Stepper/Step.js +47 -60
  24. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  25. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  26. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  27. package/lib/commonjs/components/Text/Text.js +31 -1
  28. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  29. package/lib/commonjs/components/Title/Title.js +10 -2
  30. package/lib/commonjs/components/index.js +35 -0
  31. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  32. package/lib/commonjs/icons/Icon.js +16 -0
  33. package/lib/commonjs/icons/registry.js +1 -1
  34. package/lib/commonjs/index.js +12 -0
  35. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  36. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  37. package/lib/commonjs/skeleton/index.js +58 -0
  38. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  39. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  40. package/lib/module/components/Accordion/Accordion.js +56 -56
  41. package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
  42. package/lib/module/components/Avatar/Avatar.js +19 -0
  43. package/lib/module/components/Badge/Badge.js +23 -0
  44. package/lib/module/components/Button/Button.js +37 -0
  45. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  46. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  47. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  48. package/lib/module/components/FormField/FormField.js +16 -3
  49. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  50. package/lib/module/components/IconButton/IconButton.js +20 -0
  51. package/lib/module/components/Image/Image.js +25 -1
  52. package/lib/module/components/ListItem/ListItem.js +25 -10
  53. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  54. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  55. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  56. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  57. package/lib/module/components/MessageField/MessageField.js +313 -0
  58. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  59. package/lib/module/components/PageHero/PageHero.js +41 -5
  60. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  61. package/lib/module/components/Stepper/Step.js +48 -61
  62. package/lib/module/components/Stepper/StepLabel.js +40 -10
  63. package/lib/module/components/Stepper/Stepper.js +15 -17
  64. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  65. package/lib/module/components/Text/Text.js +31 -1
  66. package/lib/module/components/TextInput/TextInput.js +17 -2
  67. package/lib/module/components/Title/Title.js +10 -2
  68. package/lib/module/components/index.js +5 -0
  69. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/lib/module/icons/Icon.js +16 -0
  71. package/lib/module/icons/registry.js +1 -1
  72. package/lib/module/index.js +2 -1
  73. package/lib/module/skeleton/Skeleton.js +229 -0
  74. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  75. package/lib/module/skeleton/index.js +6 -0
  76. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  77. package/lib/module/skeleton/useReducedMotion.js +61 -0
  78. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  79. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  80. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  81. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  82. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  83. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  84. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  85. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  86. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  87. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  88. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  89. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  90. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  91. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  92. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  93. package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
  94. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  95. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  96. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  97. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  98. package/lib/typescript/src/components/Text/Text.d.ts +20 -1
  99. package/lib/typescript/src/components/index.d.ts +8 -3
  100. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  101. package/lib/typescript/src/icons/registry.d.ts +1 -1
  102. package/lib/typescript/src/index.d.ts +1 -0
  103. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  104. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  105. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  106. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  107. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  108. package/package.json +11 -1
  109. package/src/components/Accordion/Accordion.tsx +113 -73
  110. package/src/components/ActionFooter/ActionFooter.tsx +210 -92
  111. package/src/components/Avatar/Avatar.tsx +26 -0
  112. package/src/components/Badge/Badge.tsx +27 -0
  113. package/src/components/Button/Button.tsx +40 -0
  114. package/src/components/Checkbox/Checkbox.tsx +22 -9
  115. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  116. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  117. package/src/components/FormField/FormField.tsx +19 -3
  118. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  119. package/src/components/IconButton/IconButton.tsx +27 -0
  120. package/src/components/Image/Image.tsx +25 -0
  121. package/src/components/ListItem/ListItem.tsx +21 -10
  122. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  123. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  124. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  125. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  126. package/src/components/MessageField/MessageField.tsx +543 -0
  127. package/src/components/NavArrow/NavArrow.tsx +81 -17
  128. package/src/components/PageHero/PageHero.tsx +61 -4
  129. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  130. package/src/components/Stepper/Step.tsx +52 -51
  131. package/src/components/Stepper/StepLabel.tsx +46 -9
  132. package/src/components/Stepper/Stepper.tsx +20 -15
  133. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  134. package/src/components/Text/Text.tsx +54 -0
  135. package/src/components/TextInput/TextInput.tsx +14 -1
  136. package/src/components/Title/Title.tsx +13 -2
  137. package/src/components/index.ts +8 -3
  138. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  139. package/src/icons/Icon.tsx +17 -0
  140. package/src/icons/registry.ts +1 -1
  141. package/src/index.ts +1 -0
  142. package/src/skeleton/Skeleton.tsx +298 -0
  143. package/src/skeleton/SkeletonGroup.tsx +193 -0
  144. package/src/skeleton/index.ts +10 -0
  145. package/src/skeleton/shimmer-tokens.ts +221 -0
  146. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -0,0 +1,350 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { View, Text, ScrollView } from 'react-native';
5
+ import Animated, { Extrapolation, interpolate, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
8
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
9
+ import Button from '../Button/Button';
10
+ import Disclaimer from '../Disclaimer/Disclaimer';
11
+ import IconButton from '../IconButton/IconButton';
12
+ import ActionFooter from '../ActionFooter/ActionFooter';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Forced modes
16
+ //
17
+ // `FullscreenModal` always themes itself with the `context5: 'Fullscreen Modal'`
18
+ // collection mode. This is what flips the section / list-item / hero text
19
+ // tokens to their white-on-dark values (see the Figma "Fullscreen Modal"
20
+ // context). It is intentionally NON-overridable: callers can pass any other
21
+ // modes (Color Mode, AppearanceBrand, …) but never context5. The frozen
22
+ // object keeps its identity stable so the token resolver's per-modes cache
23
+ // stays hot, and so `cloneChildrenWithModes` can use it as the
24
+ // always-wins `forcedModes` argument.
25
+ // ---------------------------------------------------------------------------
26
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
27
+ const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({
28
+ context5: 'Fullscreen Modal'
29
+ });
30
+
31
+ // Reanimated-driven ScrollView so the parallax handler runs on the UI thread.
32
+ // Module scope so the wrapped component identity is stable across renders.
33
+ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
34
+
35
+ // Parallax tuning. The hero collapses by HEIGHT only as the user scrolls up —
36
+ // its full width is preserved and the media keeps a fixed aspect ratio (it is
37
+ // cropped, never scaled or squished, like a `cover` background). When no
38
+ // explicit `heroMinHeight` is given, the hero collapses to this fraction of
39
+ // its resting height.
40
+ const HERO_MIN_HEIGHT_RATIO = 0.45;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Hero text — the eyebrow / headline / supporting / price block. Built inline
44
+ // (rather than reusing <PageHero>) so we can render BOTH a supporting
45
+ // paragraph AND a price line with the exact PageHero token gaps, and overlay
46
+ // it on the parallax media without PageHero's media/button scaffolding.
47
+ // ---------------------------------------------------------------------------
48
+
49
+ function HeroText({
50
+ eyebrow,
51
+ headline,
52
+ supportingText,
53
+ priceText,
54
+ modes
55
+ }) {
56
+ const styles = useMemo(() => {
57
+ const gap = Number(getVariableByName('PageHero/gap', modes)) || 16;
58
+ const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8;
59
+ const eyebrowStyle = {
60
+ color: getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff',
61
+ fontFamily: getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System',
62
+ fontSize: Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18,
63
+ fontWeight: String(getVariableByName('PageHero/eyebrow/fontWeight', modes) || 700),
64
+ lineHeight: Number(getVariableByName('PageHero/eyebrow/lineHeight', modes)) || 20,
65
+ textAlign: 'center'
66
+ };
67
+ const headlineStyle = {
68
+ color: getVariableByName('PageHero/headline/color', modes) || '#ffffff',
69
+ fontFamily: getVariableByName('PageHero/headline/fontFamily', modes) || 'System',
70
+ fontSize: Number(getVariableByName('PageHero/headline/fontSize', modes)) || 29,
71
+ fontWeight: String(getVariableByName('PageHero/headline/fontWeight', modes) || 900),
72
+ lineHeight: Number(getVariableByName('PageHero/headline/lineHeight', modes)) || 29,
73
+ textAlign: 'center',
74
+ width: '100%'
75
+ };
76
+ const supportingTextStyle = {
77
+ color: getVariableByName('PageHero/supportingText/color', modes) || '#ffffff',
78
+ fontFamily: getVariableByName('PageHero/supportingText/fontFamily', modes) || 'System',
79
+ fontSize: Number(getVariableByName('PageHero/supportingText/fontSize', modes)) || 12,
80
+ fontWeight: String(getVariableByName('PageHero/supportingText/fontWeight', modes) || 500),
81
+ lineHeight: Number(getVariableByName('PageHero/supportingText/lineHeight', modes)) || 16,
82
+ textAlign: 'center'
83
+ };
84
+ const priceTextStyle = {
85
+ color: getVariableByName('PageHero/body/color', modes) || '#ffffff',
86
+ fontFamily: getVariableByName('PageHero/body/fontFamily', modes) || 'System',
87
+ fontSize: Number(getVariableByName('PageHero/body/fontSize', modes)) || 12,
88
+ fontWeight: String(getVariableByName('PageHero/body/fontWeight', modes) || 500),
89
+ lineHeight: Number(getVariableByName('PageHero/body/lineHeight', modes)) || 16,
90
+ textAlign: 'center'
91
+ };
92
+ return {
93
+ container: {
94
+ width: '100%',
95
+ alignItems: 'center',
96
+ gap
97
+ },
98
+ textWrap: {
99
+ width: '100%',
100
+ alignItems: 'center',
101
+ gap: textWrapGap
102
+ },
103
+ eyebrowStyle,
104
+ headlineStyle,
105
+ supportingTextStyle,
106
+ priceTextStyle
107
+ };
108
+ }, [modes]);
109
+ return /*#__PURE__*/_jsxs(View, {
110
+ style: styles.container,
111
+ children: [/*#__PURE__*/_jsxs(View, {
112
+ style: styles.textWrap,
113
+ children: [eyebrow ? /*#__PURE__*/_jsx(Text, {
114
+ style: styles.eyebrowStyle,
115
+ children: eyebrow
116
+ }) : null, headline ? /*#__PURE__*/_jsx(Text, {
117
+ style: styles.headlineStyle,
118
+ children: headline
119
+ }) : null]
120
+ }), supportingText ? /*#__PURE__*/_jsx(Text, {
121
+ style: styles.supportingTextStyle,
122
+ children: supportingText
123
+ }) : null, priceText ? /*#__PURE__*/_jsx(Text, {
124
+ style: styles.priceTextStyle,
125
+ children: priceText
126
+ }) : null]
127
+ });
128
+ }
129
+
130
+ /**
131
+ * FullscreenModal — a full-screen takeover surface with a parallax media hero,
132
+ * a scrollable body, a floating close button, and a sticky `ActionFooter`.
133
+ *
134
+ * The component always themes itself with `context5: 'Fullscreen Modal'`
135
+ * (non-overridable) so every nested component (Section, ListItem, Button,
136
+ * Disclaimer, …) resolves the white-on-dark "fullscreen modal" token values.
137
+ * That mode is cascaded into `children`, the footer, and the hero text via
138
+ * `cloneChildrenWithModes` / the merged `modes` object.
139
+ *
140
+ * ### Parallax
141
+ * As the user scrolls up, the hero collapses by **height only** (from
142
+ * `heroHeight` to `heroMinHeight`) — its **full width is always preserved**.
143
+ * The `heroMedia` is pinned to the top at a fixed size and `cover`-cropped by
144
+ * the collapsing clip, so it keeps a perfect aspect ratio the whole time
145
+ * (never scaled or squished). Because it collapses slower than the content
146
+ * scrolls, the media lags behind for the parallax depth cue. Disable with
147
+ * `parallax={false}`.
148
+ *
149
+ * @component
150
+ * @example
151
+ * ```tsx
152
+ * <FullscreenModal
153
+ * eyebrow="Upgrade to JioFinance+"
154
+ * headline="Get more from your money."
155
+ * supportingText="JioFinance+ is your upgraded financial experience…"
156
+ * priceText="₹999/year · ₹0 until 2027"
157
+ * heroMedia={<LottiePlayer source={hero} size={{ width: 360, height: 420 }} />}
158
+ * primaryActionLabel="Upgrade for free"
159
+ * disclaimer="By upgrading, we'll check your eligibility with Experian."
160
+ * onPrimaryAction={() => upgrade()}
161
+ * onClose={() => navigation.goBack()}
162
+ * >
163
+ * <Section title="Key Benefits" slotDirection="column" slot={…} />
164
+ * <Section title="Compare plans" slotDirection="column" slot={…} />
165
+ * </FullscreenModal>
166
+ * ```
167
+ */
168
+ function FullscreenModal({
169
+ eyebrow = 'Upgrade to JioFinance+',
170
+ headline = 'Get more from your money.',
171
+ supportingText = 'JioFinance+ is your upgraded financial experience, designed to work harder in the background so your money works smarter in real life.',
172
+ priceText = '₹999/year · ₹0 until 2027',
173
+ heroMedia,
174
+ heroHeight = 420,
175
+ heroMinHeight,
176
+ parallax = true,
177
+ showClose = true,
178
+ onClose,
179
+ closeAccessibilityLabel = 'Close',
180
+ footer,
181
+ primaryActionLabel = 'Upgrade for free',
182
+ onPrimaryAction,
183
+ disclaimer = "By upgrading, we'll check your eligibility with Experian.",
184
+ backgroundColor = '#0f0d0a',
185
+ children,
186
+ modes: propModes = EMPTY_MODES,
187
+ style,
188
+ contentContainerStyle,
189
+ testID
190
+ }) {
191
+ const {
192
+ modes: globalModes
193
+ } = useTokens();
194
+
195
+ // context5 is appended last so it always wins, regardless of what the
196
+ // caller (or the global theme) passes.
197
+ const modes = useMemo(() => ({
198
+ ...globalModes,
199
+ ...propModes,
200
+ ...FULLSCREEN_MODAL_FORCED_MODES
201
+ }), [globalModes, propModes]);
202
+ const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16;
203
+ const minHeight = heroMinHeight ?? Math.round(heroHeight * HERO_MIN_HEIGHT_RATIO);
204
+ const scrollY = useSharedValue(0);
205
+ const onScroll = useAnimatedScrollHandler(event => {
206
+ scrollY.value = event.contentOffset.y;
207
+ });
208
+
209
+ // Collapse the hero by HEIGHT only as the user scrolls up. The clip's width
210
+ // never changes and the media inside is pinned full-size at the top, so the
211
+ // art is cropped (cover) rather than scaled or narrowed — it keeps a perfect
212
+ // aspect ratio the whole time. Pull-down (negative offset) is clamped, so the
213
+ // hero never grows past its resting height.
214
+ const heroAnimatedStyle = useAnimatedStyle(() => {
215
+ const height = interpolate(scrollY.value, [0, heroHeight], [heroHeight, minHeight], Extrapolation.CLAMP);
216
+ return {
217
+ height
218
+ };
219
+ });
220
+ const processedHeroMedia = useMemo(() => heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [heroMedia, modes]);
221
+ const processedChildren = useMemo(() => children ? cloneChildrenWithModes(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [children, modes]);
222
+
223
+ // The clip is full-width and top-pinned; its height is what animates. Width
224
+ // is intentionally never animated.
225
+ const heroClipBaseStyle = useMemo(() => ({
226
+ position: 'absolute',
227
+ top: 0,
228
+ left: 0,
229
+ right: 0,
230
+ overflow: 'hidden'
231
+ }), []);
232
+
233
+ // The media sits at a fixed full-size box pinned to the top of the clip, so
234
+ // the collapsing clip crops it from the bottom (cover) instead of resizing
235
+ // it. Full width, fixed height — a perfect, constant aspect ratio.
236
+ const heroMediaWrapStyle = useMemo(() => ({
237
+ position: 'absolute',
238
+ top: 0,
239
+ left: 0,
240
+ right: 0,
241
+ height: heroHeight,
242
+ alignItems: 'stretch'
243
+ }), [heroHeight]);
244
+ const heroTextRegionStyle = useMemo(() => ({
245
+ height: heroHeight,
246
+ justifyContent: 'flex-end',
247
+ paddingHorizontal: 16,
248
+ paddingBottom: 16
249
+ }), [heroHeight]);
250
+ const bodyStyle = useMemo(() => [{
251
+ backgroundColor,
252
+ gap: rootGap,
253
+ paddingTop: rootGap,
254
+ paddingBottom: 24
255
+ }, contentContainerStyle], [backgroundColor, rootGap, contentContainerStyle]);
256
+ const heroClip = /*#__PURE__*/_jsx(Animated.View, {
257
+ style: [heroClipBaseStyle, parallax ? heroAnimatedStyle : {
258
+ height: heroHeight
259
+ }],
260
+ pointerEvents: "none",
261
+ children: /*#__PURE__*/_jsx(View, {
262
+ style: heroMediaWrapStyle,
263
+ children: processedHeroMedia
264
+ })
265
+ });
266
+
267
+ // Footer: a fully custom node, or the default Button + Disclaimer column.
268
+ let footerContent = null;
269
+ if (footer) {
270
+ footerContent = footer;
271
+ } else if (primaryActionLabel) {
272
+ footerContent = /*#__PURE__*/_jsxs(View, {
273
+ style: footerColumnStyle,
274
+ children: [/*#__PURE__*/_jsx(Button, {
275
+ label: primaryActionLabel,
276
+ modes: modes,
277
+ style: fullWidthStyle,
278
+ ...(onPrimaryAction ? {
279
+ onPress: onPrimaryAction
280
+ } : {})
281
+ }), disclaimer ? /*#__PURE__*/_jsx(Disclaimer, {
282
+ disclaimer: disclaimer,
283
+ modes: modes
284
+ }) : null]
285
+ });
286
+ }
287
+ return /*#__PURE__*/_jsxs(View, {
288
+ style: [rootStyle, {
289
+ backgroundColor
290
+ }, style],
291
+ testID: testID,
292
+ children: [processedHeroMedia ? heroClip : null, /*#__PURE__*/_jsxs(AnimatedScrollView, {
293
+ style: scrollViewStyle,
294
+ contentContainerStyle: scrollContentStyle,
295
+ showsVerticalScrollIndicator: false,
296
+ onScroll: onScroll,
297
+ scrollEventThrottle: 16,
298
+ children: [/*#__PURE__*/_jsx(View, {
299
+ style: heroTextRegionStyle,
300
+ children: /*#__PURE__*/_jsx(HeroText, {
301
+ eyebrow: eyebrow,
302
+ headline: headline,
303
+ supportingText: supportingText,
304
+ priceText: priceText,
305
+ modes: modes
306
+ })
307
+ }), /*#__PURE__*/_jsx(View, {
308
+ style: bodyStyle,
309
+ children: processedChildren
310
+ })]
311
+ }), footerContent ? /*#__PURE__*/_jsx(ActionFooter, {
312
+ modes: modes,
313
+ children: footerContent
314
+ }) : null, showClose ? /*#__PURE__*/_jsx(IconButton, {
315
+ iconName: "ic_close",
316
+ modes: modes,
317
+ accessibilityLabel: closeAccessibilityLabel,
318
+ style: closeButtonStyle,
319
+ ...(onClose ? {
320
+ onPress: onClose
321
+ } : {})
322
+ }) : null]
323
+ });
324
+ }
325
+
326
+ // Module-scope style constants — never re-allocated per render.
327
+ const rootStyle = {
328
+ flex: 1,
329
+ width: '100%',
330
+ position: 'relative'
331
+ };
332
+ const scrollViewStyle = {
333
+ flex: 1
334
+ };
335
+ const scrollContentStyle = {
336
+ flexGrow: 1
337
+ };
338
+ const footerColumnStyle = {
339
+ width: '100%',
340
+ gap: 8
341
+ };
342
+ const fullWidthStyle = {
343
+ width: '100%'
344
+ };
345
+ const closeButtonStyle = {
346
+ position: 'absolute',
347
+ top: 12,
348
+ right: 12
349
+ };
350
+ export default FullscreenModal;
@@ -6,6 +6,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
6
6
  import Icon from '../../icons/Icon';
7
7
  import { usePressableWebSupport } from '../../utils/web-platform-utils';
8
8
  import { EMPTY_MODES } from '../../utils/react-utils';
9
+ import Skeleton from '../../skeleton/Skeleton';
10
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
9
11
  import { jsx as _jsx } from "react/jsx-runtime";
10
12
  // ---------------------------------------------------------------------------
11
13
  // Module-scope constants
@@ -93,6 +95,7 @@ function IconButton({
93
95
  inactiveIcon,
94
96
  inactiveSource,
95
97
  isActive = false,
98
+ loading,
96
99
  ...rest
97
100
  }) {
98
101
  // Merge explicit props with modes for token resolution. Memoize the merged
@@ -104,6 +107,13 @@ function IconButton({
104
107
  isActive
105
108
  }), [modes, isToggle, isActive]);
106
109
  const tokens = useMemo(() => resolveIconButtonTokens(componentModes, disabled), [componentModes, disabled]);
110
+
111
+ // Hook called unconditionally — short-circuit below comes AFTER all hooks
112
+ // to keep React's hook order stable across renders.
113
+ const {
114
+ active: groupActive
115
+ } = useSkeleton();
116
+ const isLoading = loading ?? groupActive;
107
117
  const [isFocused, setIsFocused] = useState(false);
108
118
  const [isHovered, setIsHovered] = useState(false);
109
119
  const userHandlersRef = useRef({});
@@ -175,6 +185,16 @@ function IconButton({
175
185
  const styleCallback = useCallback(({
176
186
  pressed
177
187
  }) => [tokens.baseContainerStyle, style, pressed && !disabled ? pressedOverlayStyle : null, isHovered && !disabled ? hoverOverlayStyle : null, isFocused && !disabled ? focusOverlayStyle : null], [tokens.baseContainerStyle, style, isHovered, isFocused, disabled]);
188
+ if (isLoading) {
189
+ const size = tokens.baseContainerStyle.width;
190
+ return /*#__PURE__*/_jsx(Skeleton, {
191
+ kind: "other",
192
+ width: size,
193
+ height: size,
194
+ style: style,
195
+ modes: componentModes
196
+ });
197
+ }
178
198
  return /*#__PURE__*/_jsx(Pressable, {
179
199
  accessibilityRole: "button",
180
200
  accessibilityLabel: undefined,
@@ -2,6 +2,8 @@
2
2
 
3
3
  import React, { useMemo } from 'react';
4
4
  import { Image as RNImage, View } from 'react-native';
5
+ import Skeleton from '../../skeleton/Skeleton';
6
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
5
7
  import { jsx as _jsx } from "react/jsx-runtime";
6
8
  function normalizeSource(imageSource) {
7
9
  if (imageSource == null) return undefined;
@@ -41,7 +43,8 @@ function Image({
41
43
  style,
42
44
  accessibilityLabel,
43
45
  accessibilityElementsHidden,
44
- importantForAccessibility
46
+ importantForAccessibility,
47
+ loading
45
48
  }) {
46
49
  const source = useMemo(() => normalizeSource(imageSource), [imageSource]);
47
50
  const layoutStyle = useMemo(() => {
@@ -63,6 +66,27 @@ function Image({
63
66
  if (borderRadius != null) s.borderRadius = borderRadius;
64
67
  return s;
65
68
  }, [ratio, width, height, borderRadius]);
69
+ const {
70
+ active: groupActive
71
+ } = useSkeleton();
72
+ const isLoading = loading ?? groupActive;
73
+ if (isLoading) {
74
+ // Match the loaded image's exact box. If height is unknown but a ratio
75
+ // is set, the skeleton uses `aspectRatio` the same way the loaded image
76
+ // would, so layout never jumps when the load resolves.
77
+ const skeletonStyle = {
78
+ width: width ?? '100%',
79
+ ...(height != null ? {
80
+ height: height
81
+ } : {
82
+ aspectRatio: ratio
83
+ })
84
+ };
85
+ return /*#__PURE__*/_jsx(Skeleton, {
86
+ kind: "image",
87
+ style: skeletonStyle
88
+ });
89
+ }
66
90
  if (!source) {
67
91
  return /*#__PURE__*/_jsx(View, {
68
92
  style: [layoutStyle, style]
@@ -32,27 +32,42 @@ const pressedOverlayStyle = {
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
34
  function resolveListItemTokens(modes) {
35
+ // Modes used to cascade into slot children (leading / supportSlot / endSlot).
36
+ // We do NOT inject an `AppearanceBrand` default here: slot content such as
37
+ // Buttons or Badges carry their own intended appearance, so forcing one onto
38
+ // them would be surprising.
35
39
  const resolvedModes = {
36
40
  ...modes,
37
41
  Context: 'ListItem'
38
42
  };
43
+
44
+ // Modes used to resolve the ListItem's OWN title + support text. Within this
45
+ // component, `AppearanceBrand` only affects `listItem/title/color` and
46
+ // `listItem/supportText/color`, so the text defaults to the "Neutral"
47
+ // appearance (in both Vertical and Horizontal layouts). A caller-supplied
48
+ // `AppearanceBrand` still wins; `Context` is always forced to 'ListItem'.
49
+ const textModes = {
50
+ AppearanceBrand: 'Neutral',
51
+ ...modes,
52
+ Context: 'ListItem'
53
+ };
39
54
  const gap = getVariableByName('listItem/gap', resolvedModes) ?? 8;
40
55
  const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0;
41
56
  const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0;
42
57
  const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0;
43
58
  const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0;
44
59
  const textWrapGap = getVariableByName('listItem/text wrap', resolvedModes) ?? 0;
45
- const titleColor = getVariableByName('listItem/title/color', resolvedModes) || '#0f0d0a';
46
- const titleFontSize = getVariableByName('listItem/title/fontSize', resolvedModes) || 14;
47
- const titleLineHeight = getVariableByName('listItem/title/lineHeight', resolvedModes) || 16;
48
- const titleFontFamily = getVariableByName('listItem/title/fontFamily', resolvedModes) || 'System';
49
- const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', resolvedModes) || 700;
60
+ const titleColor = getVariableByName('listItem/title/color', textModes) || '#0f0d0a';
61
+ const titleFontSize = getVariableByName('listItem/title/fontSize', textModes) || 14;
62
+ const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes) || 16;
63
+ const titleFontFamily = getVariableByName('listItem/title/fontFamily', textModes) || 'System';
64
+ const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', textModes) || 700;
50
65
  const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
51
- const supportColor = getVariableByName('listItem/supportText/color', resolvedModes) || '#1f1a14';
52
- const supportFontSize = getVariableByName('listItem/supportText/fontSize', resolvedModes) || 12;
53
- const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', resolvedModes) || 14;
54
- const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', resolvedModes) || 'System';
55
- const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', resolvedModes) || 500;
66
+ const supportColor = getVariableByName('listItem/supportText/color', textModes) || '#1f1a14';
67
+ const supportFontSize = getVariableByName('listItem/supportText/fontSize', textModes) || 12;
68
+ const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', textModes) || 14;
69
+ const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', textModes) || 'System';
70
+ const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', textModes) || 500;
56
71
  const supportFontWeight = typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw;
57
72
  return {
58
73
  baseContainerStyle: {
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { View } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import { getNativeLottieView } from './loadNativeLottieView';
9
+
10
+ /**
11
+ * A parsed Lottie animation. The JSON object you get from
12
+ * `require('./animation.json')` or `fetch().then(r => r.json())`. We keep the
13
+ * type intentionally loose because both `lottie-react-native` and `lottie-react`
14
+ * accept slightly different shapes — `LottiePlayer` narrows back to the
15
+ * platform-specific type internally.
16
+ */
17
+ import { jsx as _jsx } from "react/jsx-runtime";
18
+ const DEFAULT_SIZE = 117;
19
+ function resolveSize(size, modes) {
20
+ if (typeof size === 'number') return {
21
+ width: size,
22
+ height: size
23
+ };
24
+ if (size && typeof size === 'object') return size;
25
+ const width = Number(getVariableByName('media/width', modes)) || DEFAULT_SIZE;
26
+ const height = Number(getVariableByName('media/height', modes)) || DEFAULT_SIZE;
27
+ return {
28
+ width,
29
+ height
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Renders a Lottie animation using the consumer's installed
35
+ * `lottie-react-native` (native) or `lottie-react` (web) — both are declared
36
+ * as **optional peer dependencies** of `jfs-components`, so installing the
37
+ * library does not pull them in. Add the relevant package to your app only
38
+ * if you actually use `LottiePlayer`:
39
+ *
40
+ * ```sh
41
+ * # React Native (iOS / Android)
42
+ * npm install lottie-react-native
43
+ * cd ios && pod install
44
+ *
45
+ * # Web (or react-native-web)
46
+ * npm install lottie-react
47
+ * ```
48
+ *
49
+ * The web build (`LottiePlayer.web.tsx`) is picked automatically by Metro /
50
+ * webpack via platform extensions — same pattern as `MediaCard/GlassFill`.
51
+ *
52
+ * Token-driven sizing: when `size` is omitted, `LottiePlayer` reads
53
+ * `media/width` and `media/height` from the Figma variables resolver, so the
54
+ * animation matches the surrounding component's `Media / Output` mode
55
+ * automatically. This is the same sizing contract `PageHero` and
56
+ * `LottieIntroBlock` use for their `media` slots.
57
+ *
58
+ * @component
59
+ * @example
60
+ * ```tsx
61
+ * import animation from './assets/loader.json';
62
+ *
63
+ * <LottiePlayer source={animation} /> // 117 × 117 (default)
64
+ * <LottiePlayer source={animation} size={70} /> // 70 × 70
65
+ * <LottiePlayer source={animation} modes={{ 'Media / Output': 'S' }} /> // 20 × 20
66
+ * <PageHero media={<LottiePlayer source={animation} />} />
67
+ * ```
68
+ */
69
+ function LottiePlayer({
70
+ source,
71
+ size,
72
+ autoPlay = true,
73
+ loop = true,
74
+ modes: propModes = EMPTY_MODES,
75
+ style,
76
+ accessibilityLabel,
77
+ testID
78
+ }) {
79
+ const {
80
+ modes: globalModes
81
+ } = useTokens();
82
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
83
+ ...globalModes,
84
+ ...propModes
85
+ }, [globalModes, propModes]);
86
+ const {
87
+ width,
88
+ height
89
+ } = useMemo(() => resolveSize(size, modes), [size, modes]);
90
+ const NativeLottieView = useMemo(() => getNativeLottieView(), []);
91
+ return /*#__PURE__*/_jsx(View, {
92
+ style: [{
93
+ width,
94
+ height
95
+ }, style],
96
+ testID: testID,
97
+ accessibilityLabel: accessibilityLabel,
98
+ accessibilityElementsHidden: accessibilityLabel ? undefined : true,
99
+ importantForAccessibility: accessibilityLabel ? 'auto' : 'no',
100
+ children: /*#__PURE__*/_jsx(NativeLottieView, {
101
+ source: source,
102
+ autoPlay: autoPlay,
103
+ loop: loop,
104
+ style: {
105
+ width: '100%',
106
+ height: '100%'
107
+ }
108
+ })
109
+ });
110
+ }
111
+ export default /*#__PURE__*/React.memo(LottiePlayer);
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ import React, { useMemo } from 'react';
4
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
5
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
6
+ import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import { getWebLottieView } from './loadWebLottieView';
8
+ import { jsx as _jsx } from "react/jsx-runtime";
9
+ const DEFAULT_SIZE = 117;
10
+ function resolveSize(size, modes) {
11
+ if (typeof size === 'number') return {
12
+ width: size,
13
+ height: size
14
+ };
15
+ if (size && typeof size === 'object') return size;
16
+ const width = Number(getVariableByName('media/width', modes)) || DEFAULT_SIZE;
17
+ const height = Number(getVariableByName('media/height', modes)) || DEFAULT_SIZE;
18
+ return {
19
+ width,
20
+ height
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Web build of `LottiePlayer` — picked automatically by webpack /
26
+ * Metro-for-web via the `.web.tsx` platform extension. Uses `lottie-react`
27
+ * (which wraps `lottie-web`) and renders a plain DOM container.
28
+ *
29
+ * Public API mirrors `LottiePlayer.tsx` (native). See that file for the
30
+ * documented prop reference and usage patterns.
31
+ */
32
+ function LottiePlayer({
33
+ source,
34
+ size,
35
+ autoPlay = true,
36
+ loop = true,
37
+ modes: propModes = EMPTY_MODES,
38
+ style,
39
+ accessibilityLabel,
40
+ testID
41
+ }) {
42
+ const {
43
+ modes: globalModes
44
+ } = useTokens();
45
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
46
+ ...globalModes,
47
+ ...propModes
48
+ }, [globalModes, propModes]);
49
+ const {
50
+ width,
51
+ height
52
+ } = useMemo(() => resolveSize(size, modes), [size, modes]);
53
+ const WebLottieView = useMemo(() => getWebLottieView(), []);
54
+ return /*#__PURE__*/_jsx("div", {
55
+ style: {
56
+ width,
57
+ height,
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ ...style
62
+ },
63
+ "data-testid": testID,
64
+ "aria-label": accessibilityLabel,
65
+ "aria-hidden": accessibilityLabel ? undefined : true,
66
+ children: /*#__PURE__*/_jsx(WebLottieView, {
67
+ animationData: source,
68
+ autoplay: autoPlay,
69
+ loop: loop,
70
+ style: {
71
+ width: '100%',
72
+ height: '100%'
73
+ }
74
+ })
75
+ });
76
+ }
77
+ export default /*#__PURE__*/React.memo(LottiePlayer);