jfs-components 0.0.86 → 0.0.95
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 +8 -0
- package/lib/commonjs/assets.d.js +1 -0
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +97 -50
- package/lib/commonjs/components/Icon/Icon.js +112 -0
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/assets.d.js +1 -0
- package/lib/module/components/FullscreenModal/FullscreenModal.js +99 -52
- package/lib/module/components/Icon/Icon.js +106 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +35 -21
- package/lib/typescript/src/components/Icon/Icon.d.ts +75 -0
- package/lib/typescript/src/components/index.d.ts +1 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/assets.d.ts +24 -0
- package/src/components/FullscreenModal/FullscreenModal.tsx +124 -61
- package/src/components/Icon/Icon.tsx +167 -0
- package/src/components/index.ts +1 -0
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/icons/registry.ts +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useMemo } from 'react';
|
|
4
|
-
import { View, Text,
|
|
3
|
+
import React, { useMemo, useRef } from 'react';
|
|
4
|
+
import { View, Text, Animated } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
6
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
7
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -28,6 +28,19 @@ const FULLSCREEN_MODAL_FORCED_MODES = Object.freeze({
|
|
|
28
28
|
context5: 'Fullscreen Modal'
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Default modes
|
|
33
|
+
//
|
|
34
|
+
// A FullscreenModal is a "JioPlus" surface, so it defaults the `Page type`
|
|
35
|
+
// collection to `'JioPlus'`. Unlike the forced modes above this IS
|
|
36
|
+
// overridable — it is applied before the caller's `modes`, so passing
|
|
37
|
+
// `modes={{ 'Page type': 'SubPage' }}` still wins. Frozen for stable identity
|
|
38
|
+
// (keeps the token resolver's per-modes cache hot).
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
const FULLSCREEN_MODAL_DEFAULT_MODES = Object.freeze({
|
|
41
|
+
'Page type': 'JioPlus'
|
|
42
|
+
});
|
|
43
|
+
|
|
31
44
|
// ---------------------------------------------------------------------------
|
|
32
45
|
// Hero text — the eyebrow / headline / supporting / price block. Built inline
|
|
33
46
|
// (rather than reusing <PageHero>) so we can render BOTH a supporting
|
|
@@ -127,12 +140,21 @@ function HeroText({
|
|
|
127
140
|
* That mode is cascaded into `children`, the footer, and the hero text via
|
|
128
141
|
* `cloneChildrenWithModes` / the merged `modes` object.
|
|
129
142
|
*
|
|
130
|
-
* ###
|
|
131
|
-
* The `heroMedia` is
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
143
|
+
* ### Background media
|
|
144
|
+
* The `heroMedia` is a single full-bleed background pinned to the top of the
|
|
145
|
+
* modal at the full width and its own natural aspect ratio. It lives at the
|
|
146
|
+
* ROOT — behind both the scrolling content and the (transparent) footer — so
|
|
147
|
+
* it fills the whole surface and is NEVER clipped to the content height. It
|
|
148
|
+
* also contributes ZERO scroll height: the scroll extent is driven purely by
|
|
149
|
+
* the in-flow foreground (hero text + `children`), so the number of body
|
|
150
|
+
* elements dictates how far the surface scrolls. It still scrolls in lockstep
|
|
151
|
+
* WITH the content (the background is translated by the scroll offset), so the
|
|
152
|
+
* content reads as sitting ON one continuous image that moves with it — there
|
|
153
|
+
* is no parallax and no separate solid body box.
|
|
154
|
+
*
|
|
155
|
+
* Pass a background sized to the full width at its natural ratio
|
|
156
|
+
* (e.g. `<Image imageSource={bg} ratio={1080 / 4140} />`). Use an asset at
|
|
157
|
+
* least as tall as the surface so it covers the full modal.
|
|
136
158
|
*
|
|
137
159
|
* @component
|
|
138
160
|
* @example
|
|
@@ -142,7 +164,7 @@ function HeroText({
|
|
|
142
164
|
* headline="Get more from your money."
|
|
143
165
|
* supportingText="JioFinance+ is your upgraded financial experience…"
|
|
144
166
|
* priceText="₹999/year · ₹0 until 2027"
|
|
145
|
-
* heroMedia={<Image imageSource={hero} ratio={
|
|
167
|
+
* heroMedia={<Image imageSource={hero} ratio={1080 / 4140} />}
|
|
146
168
|
* primaryActionLabel="Upgrade for free"
|
|
147
169
|
* disclaimer="By upgrading, we'll check your eligibility with Experian."
|
|
148
170
|
* onPrimaryAction={() => upgrade()}
|
|
@@ -167,7 +189,6 @@ function FullscreenModal({
|
|
|
167
189
|
primaryActionLabel = 'Upgrade for free',
|
|
168
190
|
onPrimaryAction,
|
|
169
191
|
disclaimer = "By upgrading, we'll check your eligibility with Experian.",
|
|
170
|
-
backgroundColor = '#0f0d0a',
|
|
171
192
|
children,
|
|
172
193
|
modes: propModes = EMPTY_MODES,
|
|
173
194
|
style,
|
|
@@ -178,31 +199,56 @@ function FullscreenModal({
|
|
|
178
199
|
modes: globalModes
|
|
179
200
|
} = useTokens();
|
|
180
201
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
202
|
+
// Merge order (low → high priority):
|
|
203
|
+
// global theme → component defaults (Page type: JioPlus) → caller modes →
|
|
204
|
+
// forced modes (context5). So `Page type` defaults to JioPlus but the
|
|
205
|
+
// caller can override it, while `context5` always wins. This single `modes`
|
|
206
|
+
// object is what cascades to the body, hero media, and the ActionFooter.
|
|
183
207
|
const modes = useMemo(() => ({
|
|
184
208
|
...globalModes,
|
|
209
|
+
...FULLSCREEN_MODAL_DEFAULT_MODES,
|
|
185
210
|
...propModes,
|
|
186
211
|
...FULLSCREEN_MODAL_FORCED_MODES
|
|
187
212
|
}), [globalModes, propModes]);
|
|
188
213
|
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16;
|
|
214
|
+
|
|
215
|
+
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
216
|
+
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
217
|
+
// behind the transparent footer), but we translate it up by the exact scroll
|
|
218
|
+
// offset so it moves in lockstep with the content — i.e. it scrolls WITH the
|
|
219
|
+
// body without ever contributing to the scroll height.
|
|
220
|
+
const scrollY = useRef(new Animated.Value(0)).current;
|
|
221
|
+
const onScroll = useMemo(() => Animated.event([{
|
|
222
|
+
nativeEvent: {
|
|
223
|
+
contentOffset: {
|
|
224
|
+
y: scrollY
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}], {
|
|
228
|
+
useNativeDriver: true
|
|
229
|
+
}), [scrollY]);
|
|
230
|
+
const heroTranslateY = useMemo(() => Animated.multiply(scrollY, -1), [scrollY]);
|
|
189
231
|
const processedHeroMedia = useMemo(() => heroMedia ? cloneChildrenWithModes(heroMedia, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [heroMedia, modes]);
|
|
190
232
|
const processedChildren = useMemo(() => children ? cloneChildrenWithModes(children, modes, FULLSCREEN_MODAL_FORCED_MODES) : null, [children, modes]);
|
|
191
233
|
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
234
|
+
// The hero text region always reserves `heroHeight` and anchors its content
|
|
235
|
+
// to the bottom, so the eyebrow/headline block sits in the lower part of the
|
|
236
|
+
// first screenful — over the background media when present, in flow
|
|
237
|
+
// otherwise.
|
|
238
|
+
const heroTextRegionStyle = useMemo(() => ({
|
|
195
239
|
minHeight: heroHeight,
|
|
196
240
|
justifyContent: 'flex-end',
|
|
197
241
|
paddingHorizontal: 16,
|
|
198
242
|
paddingBottom: 16
|
|
199
243
|
}), [heroHeight]);
|
|
244
|
+
|
|
245
|
+
// Body is intentionally transparent — the background media shows through
|
|
246
|
+
// behind it. There is no solid "body box" stacked on top of the image.
|
|
200
247
|
const bodyStyle = useMemo(() => [{
|
|
201
|
-
backgroundColor,
|
|
202
248
|
gap: rootGap,
|
|
203
249
|
paddingTop: rootGap,
|
|
204
250
|
paddingBottom: 24
|
|
205
|
-
}, contentContainerStyle], [
|
|
251
|
+
}, contentContainerStyle], [rootGap, contentContainerStyle]);
|
|
206
252
|
const heroTextNode = /*#__PURE__*/_jsx(HeroText, {
|
|
207
253
|
eyebrow: eyebrow,
|
|
208
254
|
headline: headline,
|
|
@@ -211,22 +257,6 @@ function FullscreenModal({
|
|
|
211
257
|
modes: modes
|
|
212
258
|
});
|
|
213
259
|
|
|
214
|
-
// The hero scrolls inline with the body (no parallax). When media is present
|
|
215
|
-
// it is laid out full modal width and takes its height from its own aspect
|
|
216
|
-
// ratio; the hero text is overlaid on top, anchored to the bottom. Without
|
|
217
|
-
// media the text simply renders in flow at the fallback height.
|
|
218
|
-
const hero = processedHeroMedia ? /*#__PURE__*/_jsxs(View, {
|
|
219
|
-
style: heroMediaContainerStyle,
|
|
220
|
-
children: [processedHeroMedia, /*#__PURE__*/_jsx(View, {
|
|
221
|
-
style: heroTextOverlayStyle,
|
|
222
|
-
pointerEvents: "box-none",
|
|
223
|
-
children: heroTextNode
|
|
224
|
-
})]
|
|
225
|
-
}) : /*#__PURE__*/_jsx(View, {
|
|
226
|
-
style: heroTextFallbackStyle,
|
|
227
|
-
children: heroTextNode
|
|
228
|
-
});
|
|
229
|
-
|
|
230
260
|
// Footer: a fully custom node, or the default Button + Disclaimer column.
|
|
231
261
|
let footerContent = null;
|
|
232
262
|
if (footer) {
|
|
@@ -249,22 +279,36 @@ function FullscreenModal({
|
|
|
249
279
|
});
|
|
250
280
|
}
|
|
251
281
|
return /*#__PURE__*/_jsxs(View, {
|
|
252
|
-
style: [rootStyle,
|
|
253
|
-
backgroundColor
|
|
254
|
-
}, style],
|
|
282
|
+
style: [rootStyle, style],
|
|
255
283
|
testID: testID,
|
|
256
|
-
children: [/*#__PURE__*/
|
|
284
|
+
children: [processedHeroMedia ? /*#__PURE__*/_jsx(Animated.View, {
|
|
285
|
+
style: [heroBackgroundStyle, {
|
|
286
|
+
transform: [{
|
|
287
|
+
translateY: heroTranslateY
|
|
288
|
+
}]
|
|
289
|
+
}],
|
|
290
|
+
pointerEvents: "none",
|
|
291
|
+
children: processedHeroMedia
|
|
292
|
+
}) : null, /*#__PURE__*/_jsx(Animated.ScrollView, {
|
|
257
293
|
style: scrollViewStyle,
|
|
258
294
|
contentContainerStyle: scrollContentStyle,
|
|
259
|
-
showsVerticalScrollIndicator: false
|
|
295
|
+
showsVerticalScrollIndicator: false,
|
|
296
|
+
onScroll: onScroll,
|
|
297
|
+
scrollEventThrottle: 16
|
|
260
298
|
// Tap an input in the body and it focuses on the FIRST tap, even when
|
|
261
299
|
// the keyboard is already open (default 'never' eats that tap).
|
|
262
300
|
,
|
|
263
301
|
keyboardShouldPersistTaps: "handled",
|
|
264
|
-
children:
|
|
265
|
-
style:
|
|
266
|
-
children:
|
|
267
|
-
|
|
302
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
303
|
+
style: foregroundFlowStyle,
|
|
304
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
305
|
+
style: heroTextRegionStyle,
|
|
306
|
+
children: heroTextNode
|
|
307
|
+
}), processedChildren ? /*#__PURE__*/_jsx(View, {
|
|
308
|
+
style: bodyStyle,
|
|
309
|
+
children: processedChildren
|
|
310
|
+
}) : null]
|
|
311
|
+
})
|
|
268
312
|
}), footerContent ? /*#__PURE__*/_jsx(ActionFooter, {
|
|
269
313
|
modes: modes,
|
|
270
314
|
children: footerContent
|
|
@@ -300,16 +344,19 @@ const closeButtonStyle = {
|
|
|
300
344
|
top: 12,
|
|
301
345
|
right: 12
|
|
302
346
|
};
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
347
|
+
// Root-level full-bleed background media. Pinned to the top at full modal
|
|
348
|
+
// width; the media inside keeps its own natural aspect ratio (only `top` is
|
|
349
|
+
// pinned — no `bottom`/`overflow` clip), so it is NEVER cut to the content
|
|
350
|
+
// height and fills the surface behind the scrolling content and the footer.
|
|
351
|
+
// Living outside the ScrollView, it adds nothing to the scroll height.
|
|
352
|
+
const heroBackgroundStyle = {
|
|
353
|
+
position: 'absolute',
|
|
354
|
+
top: 0,
|
|
355
|
+
left: 0,
|
|
356
|
+
right: 0
|
|
307
357
|
};
|
|
308
|
-
//
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
justifyContent: 'flex-end',
|
|
312
|
-
paddingHorizontal: 16,
|
|
313
|
-
paddingBottom: 16
|
|
358
|
+
// The foreground always flows normally — its content drives the scroll height.
|
|
359
|
+
const foregroundFlowStyle = {
|
|
360
|
+
width: '100%'
|
|
314
361
|
};
|
|
315
362
|
export default FullscreenModal;
|
|
@@ -0,0 +1,106 @@
|
|
|
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, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
8
|
+
import BaseIcon from '../../icons/Icon';
|
|
9
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
10
|
+
function resolveIconTokens(modes) {
|
|
11
|
+
const iconColor = getVariableByName('icon/color', modes) || '#ad8444';
|
|
12
|
+
const iconSize = getVariableByName('icon/size', modes) || 18;
|
|
13
|
+
const paddingLeft = getVariableByName('icon/padding/left', modes) || 0;
|
|
14
|
+
const paddingTop = getVariableByName('icon/padding/top', modes) || 0;
|
|
15
|
+
const paddingRight = getVariableByName('icon/padding/right', modes) || 0;
|
|
16
|
+
const paddingBottom = getVariableByName('icon/padding/bottom', modes) || 0;
|
|
17
|
+
return {
|
|
18
|
+
containerStyle: {
|
|
19
|
+
flexDirection: 'column',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
overflow: 'hidden',
|
|
23
|
+
paddingLeft,
|
|
24
|
+
paddingTop,
|
|
25
|
+
paddingRight,
|
|
26
|
+
paddingBottom
|
|
27
|
+
},
|
|
28
|
+
iconColor,
|
|
29
|
+
iconSize
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Icon component — a design-token-driven wrapper around a single glyph.
|
|
35
|
+
*
|
|
36
|
+
* It mirrors the Figma "Icon" component: a padded, centered container whose
|
|
37
|
+
* color and size are resolved from the `icon/*` design tokens via `modes`.
|
|
38
|
+
* The glyph itself can be supplied three ways, in order of precedence:
|
|
39
|
+
*
|
|
40
|
+
* 1. `children` — a real slot for any node (custom SVG component, nested
|
|
41
|
+
* `Icon`, etc.). `modes` cascade into the slot automatically.
|
|
42
|
+
* 2. `iconName` — a registry icon in the `ic_something` format.
|
|
43
|
+
* 3. `source` — a {@link UnifiedSource} fallback (remote URI, inline SVG XML,
|
|
44
|
+
* `require()` asset, SVG component, or React element), tinted with the
|
|
45
|
+
* mode-resolved icon color.
|
|
46
|
+
*
|
|
47
|
+
* `color` and `size` props let consumers override the token values per
|
|
48
|
+
* instance without touching `modes`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* // Built-in registry icon (default path).
|
|
53
|
+
* <Icon iconName="ic_card" modes={{ 'Color Mode': 'Light' }} />
|
|
54
|
+
*
|
|
55
|
+
* // Per-instance overrides.
|
|
56
|
+
* <Icon iconName="ic_ccv" color="#5c00b5" size={24} />
|
|
57
|
+
*
|
|
58
|
+
* // Fallback to an external source when the name isn't in the registry.
|
|
59
|
+
* <Icon source="https://cdn.example.com/glyph.svg" />
|
|
60
|
+
*
|
|
61
|
+
* // Slot: render any node as the icon.
|
|
62
|
+
* <Icon><BrandLogo /></Icon>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
function Icon({
|
|
66
|
+
iconName,
|
|
67
|
+
source,
|
|
68
|
+
children,
|
|
69
|
+
color,
|
|
70
|
+
size,
|
|
71
|
+
modes: propModes = EMPTY_MODES,
|
|
72
|
+
style: styleProp,
|
|
73
|
+
...rest
|
|
74
|
+
}) {
|
|
75
|
+
const {
|
|
76
|
+
modes: globalModes
|
|
77
|
+
} = useTokens();
|
|
78
|
+
const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
|
|
79
|
+
...globalModes,
|
|
80
|
+
...propModes
|
|
81
|
+
}, [globalModes, propModes]);
|
|
82
|
+
const tokens = useMemo(() => resolveIconTokens(modes), [modes]);
|
|
83
|
+
const composedStyle = useMemo(() => styleProp ? [tokens.containerStyle, styleProp] : tokens.containerStyle, [tokens.containerStyle, styleProp]);
|
|
84
|
+
const hasSlot = React.Children.count(children) > 0;
|
|
85
|
+
|
|
86
|
+
// Only fall back to the default glyph when nothing at all is provided so an
|
|
87
|
+
// explicit `source` (without an `iconName`) isn't shadowed by `ic_card`.
|
|
88
|
+
const resolvedName = iconName ?? (source === undefined ? 'ic_card' : undefined);
|
|
89
|
+
const iconColor = color ?? tokens.iconColor;
|
|
90
|
+
const iconSize = size ?? tokens.iconSize;
|
|
91
|
+
return /*#__PURE__*/_jsx(View, {
|
|
92
|
+
style: composedStyle,
|
|
93
|
+
...rest,
|
|
94
|
+
children: hasSlot ? cloneChildrenWithModes(children, modes) : /*#__PURE__*/_jsx(BaseIcon, {
|
|
95
|
+
name: resolvedName,
|
|
96
|
+
...(source !== undefined ? {
|
|
97
|
+
source
|
|
98
|
+
} : {}),
|
|
99
|
+
size: iconSize,
|
|
100
|
+
color: iconColor,
|
|
101
|
+
accessibilityElementsHidden: true,
|
|
102
|
+
importantForAccessibility: "no"
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export default /*#__PURE__*/React.memo(Icon);
|
|
@@ -44,6 +44,7 @@ export { default as MonthlyStatusGrid, CalendarGlyph } from './MonthlyStatusGrid
|
|
|
44
44
|
export { default as Gauge } from './Gauge/Gauge';
|
|
45
45
|
export { default as HoldingsCard } from './HoldingsCard/HoldingsCard';
|
|
46
46
|
export { default as HStack } from './HStack/HStack';
|
|
47
|
+
export { default as Icon } from './Icon/Icon';
|
|
47
48
|
export { default as IconButton } from './IconButton/IconButton';
|
|
48
49
|
export { default as IconCapsule } from './IconCapsule/IconCapsule';
|
|
49
50
|
export { default as Image } from './Image/Image';
|