jfs-components 0.0.95 → 0.1.0
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 +10 -0
- package/lib/commonjs/components/Drawer/Drawer.js +146 -82
- package/lib/commonjs/components/Dropdown/Dropdown.js +37 -18
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +22 -1
- package/lib/commonjs/components/Spinner/Spinner.js +5 -1
- package/lib/commonjs/components/TestimonialsCard/TestimonialsCard.js +121 -0
- package/lib/commonjs/components/index.js +7 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/skeleton/Skeleton.js +10 -2
- package/lib/module/components/Drawer/Drawer.js +148 -84
- package/lib/module/components/Dropdown/Dropdown.js +37 -18
- package/lib/module/components/FullscreenModal/FullscreenModal.js +22 -1
- package/lib/module/components/Spinner/Spinner.js +6 -2
- package/lib/module/components/TestimonialsCard/TestimonialsCard.js +116 -0
- package/lib/module/components/index.js +1 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/skeleton/Skeleton.js +11 -3
- package/lib/typescript/src/components/Drawer/Drawer.d.ts +23 -4
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +7 -1
- package/lib/typescript/src/components/TestimonialsCard/TestimonialsCard.d.ts +51 -0
- package/lib/typescript/src/components/index.d.ts +2 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Drawer/Drawer.tsx +94 -15
- package/src/components/Dropdown/Dropdown.tsx +38 -18
- package/src/components/FullscreenModal/FullscreenModal.tsx +29 -2
- package/src/components/Spinner/Spinner.tsx +2 -2
- package/src/components/TestimonialsCard/TestimonialsCard.tsx +162 -0
- package/src/components/index.ts +2 -1
- package/src/icons/registry.ts +1 -1
- package/src/skeleton/Skeleton.tsx +10 -3
|
@@ -21,12 +21,20 @@ const SOLID_OVERLAY_COLOR = 'rgba(255, 255, 255, 1)';
|
|
|
21
21
|
// `pointerEvents: 'none'` lives on the style (not the deprecated prop) so it
|
|
22
22
|
// works on both native and React Native Web without warnings.
|
|
23
23
|
const absoluteFillStyle = {
|
|
24
|
-
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
top: 0,
|
|
26
|
+
left: 0,
|
|
27
|
+
right: 0,
|
|
28
|
+
bottom: 0,
|
|
25
29
|
overflow: 'hidden',
|
|
26
30
|
pointerEvents: 'none'
|
|
27
31
|
};
|
|
28
32
|
const solidOverlayStyle = {
|
|
29
|
-
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
top: 0,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
bottom: 0,
|
|
30
38
|
backgroundColor: SOLID_OVERLAY_COLOR
|
|
31
39
|
};
|
|
32
40
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useEffect,
|
|
3
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
4
4
|
import { Platform, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
|
|
5
|
-
import { Gesture, GestureDetector,
|
|
5
|
+
import { Gesture, GestureDetector, ScrollView } from 'react-native-gesture-handler';
|
|
6
6
|
import Animated, { runOnJS, useAnimatedProps, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
7
7
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
8
8
|
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
@@ -40,11 +40,17 @@ function rubberBand(value, min, max, friction = 0.55) {
|
|
|
40
40
|
}
|
|
41
41
|
return value;
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Imperative handle exposed via `ref` for programmatic control of the drawer.
|
|
46
|
+
* Each method animates using the same spring as gesture-driven transitions.
|
|
47
|
+
*/
|
|
48
|
+
|
|
43
49
|
/**
|
|
44
50
|
* Drawer component with nested scrolling support.
|
|
45
51
|
* Uses react-native-gesture-handler and react-native-reanimated.
|
|
46
52
|
*/
|
|
47
|
-
function
|
|
53
|
+
function DrawerInner({
|
|
48
54
|
modes = EMPTY_MODES,
|
|
49
55
|
style,
|
|
50
56
|
title,
|
|
@@ -53,6 +59,7 @@ function Drawer({
|
|
|
53
59
|
collapsedHeight = 200,
|
|
54
60
|
expandedRatio = 0.90,
|
|
55
61
|
initialState = 'collapsed',
|
|
62
|
+
state,
|
|
56
63
|
contentStyle,
|
|
57
64
|
sheetStyle,
|
|
58
65
|
accessibilityLabel,
|
|
@@ -61,7 +68,7 @@ function Drawer({
|
|
|
61
68
|
showsVerticalScrollIndicator = false,
|
|
62
69
|
bottomInset = 80,
|
|
63
70
|
onStateChange
|
|
64
|
-
}) {
|
|
71
|
+
}, ref) {
|
|
65
72
|
const {
|
|
66
73
|
height: screenHeight
|
|
67
74
|
} = useWindowDimensions();
|
|
@@ -123,15 +130,58 @@ function Drawer({
|
|
|
123
130
|
translateY.value = withSpring(destination, SPRING_CONFIG);
|
|
124
131
|
}, [translateY]);
|
|
125
132
|
|
|
126
|
-
// Update JS
|
|
133
|
+
// Update the JS-side mode. Pure: only schedules the state update. Side
|
|
134
|
+
// effects (notifying the parent via onStateChange) MUST NOT live inside the
|
|
135
|
+
// setState updater — doing so calls the parent's setState during React's
|
|
136
|
+
// render phase, which throws "Cannot update a component while rendering a
|
|
137
|
+
// different component" and causes an infinite update loop. We report changes
|
|
138
|
+
// from a dedicated effect below instead.
|
|
127
139
|
const updateMode = useCallback(newMode => {
|
|
128
|
-
setMode(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
setMode(newMode);
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
143
|
+
// Notify the parent exactly once per settled mode change, after commit.
|
|
144
|
+
const reportedModeRef = useRef(mode);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (reportedModeRef.current !== mode) {
|
|
147
|
+
reportedModeRef.current = mode;
|
|
148
|
+
onStateChange?.(mode);
|
|
149
|
+
}
|
|
150
|
+
}, [mode, onStateChange]);
|
|
151
|
+
|
|
152
|
+
// Programmatic transition (JS thread). Reuses the same spring as gestures:
|
|
153
|
+
// animates `translateY`, keeps the scroll-gate (`isFullyExpanded`) in sync,
|
|
154
|
+
// and updates `mode` (which notifies via the onStateChange effect above).
|
|
155
|
+
const applyMode = useCallback(newMode => {
|
|
156
|
+
const target = newMode === 'expanded' ? minTranslateY : maxTranslateY;
|
|
157
|
+
translateY.value = withSpring(target, SPRING_CONFIG);
|
|
158
|
+
isFullyExpanded.value = newMode === 'expanded';
|
|
159
|
+
setMode(newMode);
|
|
160
|
+
}, [minTranslateY, maxTranslateY, translateY, isFullyExpanded]);
|
|
161
|
+
|
|
162
|
+
// Controlled mode: react ONLY to genuine changes of the `state` prop, tracked
|
|
163
|
+
// against its previous value. We must NOT reconcile `state` against the
|
|
164
|
+
// internal `mode` on every render: a gesture updates `mode` optimistically
|
|
165
|
+
// one render before the parent echoes it back through `onStateChange` into
|
|
166
|
+
// `state`, so a `state !== mode` check would "correct" the gesture and fight
|
|
167
|
+
// the echo, ping-ponging forever (Maximum update depth exceeded).
|
|
168
|
+
// `applyMode` is idempotent, so re-applying the already-current mode is a
|
|
169
|
+
// no-op when the parent simply mirrors our own change back.
|
|
170
|
+
const prevStateProp = useRef(state);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (state === undefined) return;
|
|
173
|
+
if (state === prevStateProp.current) return;
|
|
174
|
+
prevStateProp.current = state;
|
|
175
|
+
applyMode(state);
|
|
176
|
+
}, [state, applyMode]);
|
|
177
|
+
|
|
178
|
+
// Imperative API for parents holding a ref.
|
|
179
|
+
useImperativeHandle(ref, () => ({
|
|
180
|
+
expand: () => applyMode('expanded'),
|
|
181
|
+
collapse: () => applyMode('collapsed'),
|
|
182
|
+
toggle: () => applyMode(mode === 'expanded' ? 'collapsed' : 'expanded'),
|
|
183
|
+
getState: () => mode
|
|
184
|
+
}), [applyMode, mode]);
|
|
135
185
|
|
|
136
186
|
// Gesture policy:
|
|
137
187
|
// • activeOffsetY: require a clear *vertical* drag (10px) before this
|
|
@@ -304,84 +354,96 @@ function Drawer({
|
|
|
304
354
|
default: {}
|
|
305
355
|
});
|
|
306
356
|
const defaultAccessibilityLabel = accessibilityLabel || title || 'Drawer';
|
|
307
|
-
return
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
return (
|
|
358
|
+
/*#__PURE__*/
|
|
359
|
+
// IMPORTANT: the host is a plain box-none View, NOT a GestureHandlerRootView.
|
|
360
|
+
// On Android, GestureHandlerRootView renders a *native* root view that
|
|
361
|
+
// intercepts every touch within its bounds (to feed the gesture system) and
|
|
362
|
+
// does NOT honor pointerEvents="box-none". Because this host fills the whole
|
|
363
|
+
// screen as an overlay, using GestureHandlerRootView here swallowed all
|
|
364
|
+
// touches to content rendered behind the drawer (buttons, page content).
|
|
365
|
+
// Per the standard react-native-gesture-handler architecture, a single
|
|
366
|
+
// GestureHandlerRootView must wrap the app root; this overlay only needs to
|
|
367
|
+
// let touches fall through where the sheet isn't.
|
|
368
|
+
_jsx(View, {
|
|
369
|
+
style: [styles.host, style],
|
|
370
|
+
pointerEvents: "box-none",
|
|
371
|
+
children: /*#__PURE__*/_jsx(GestureDetector, {
|
|
372
|
+
gesture: gesture,
|
|
373
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
374
|
+
style: [styles.sheet, {
|
|
375
|
+
// Constraint the height strictly to the expanded height
|
|
376
|
+
// This ensures the ScrollView has a finite frame to scroll within
|
|
377
|
+
height: expandedHeight,
|
|
378
|
+
backgroundColor,
|
|
329
379
|
borderTopLeftRadius: radius,
|
|
330
|
-
borderTopRightRadius: radius
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
380
|
+
borderTopRightRadius: radius
|
|
381
|
+
}, shadowStyle, sheetStyle, animatedStyle],
|
|
382
|
+
accessible: true,
|
|
383
|
+
...(Platform.OS === 'web' ? {
|
|
384
|
+
accessibilityRole: 'dialog'
|
|
385
|
+
} : undefined),
|
|
386
|
+
accessibilityLabel: undefined,
|
|
387
|
+
accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
|
|
388
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
389
|
+
style: [styles.sheetInner, {
|
|
390
|
+
borderTopLeftRadius: radius,
|
|
391
|
+
borderTopRightRadius: radius,
|
|
392
|
+
paddingLeft,
|
|
393
|
+
paddingRight,
|
|
394
|
+
paddingBottom,
|
|
395
|
+
rowGap: drawerGap
|
|
339
396
|
}],
|
|
340
|
-
children: /*#__PURE__*/_jsx(View, {
|
|
397
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
398
|
+
style: [styles.handleArea, !title && !header && {
|
|
399
|
+
paddingBottom: 0
|
|
400
|
+
}],
|
|
401
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
402
|
+
style: [{
|
|
403
|
+
backgroundColor: handleColor,
|
|
404
|
+
width: handleWidth,
|
|
405
|
+
height: handleHeight,
|
|
406
|
+
borderRadius: handleRadius
|
|
407
|
+
}]
|
|
408
|
+
})
|
|
409
|
+
}), header, title && /*#__PURE__*/_jsx(Text, {
|
|
341
410
|
style: [{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
overScrollMode: "always",
|
|
375
|
-
onScroll: useAnimatedScrollHandler(event => {
|
|
376
|
-
scrollY.value = event.contentOffset.y;
|
|
377
|
-
}),
|
|
378
|
-
scrollEventThrottle: 16,
|
|
379
|
-
children: children
|
|
380
|
-
})]
|
|
411
|
+
color: titleColor,
|
|
412
|
+
fontSize: titleSize,
|
|
413
|
+
fontWeight: titleWeight,
|
|
414
|
+
lineHeight: titleLineHeight,
|
|
415
|
+
marginBottom: titlePaddingBottom
|
|
416
|
+
}],
|
|
417
|
+
children: title
|
|
418
|
+
}), /*#__PURE__*/_jsx(AnimatedScrollView, {
|
|
419
|
+
ref: scrollRef,
|
|
420
|
+
style: [styles.content, contentStyle],
|
|
421
|
+
contentContainerStyle: [{
|
|
422
|
+
paddingBottom: paddingBottom + bottomInset,
|
|
423
|
+
gap: drawerGap,
|
|
424
|
+
flexDirection: 'column',
|
|
425
|
+
alignItems: 'stretch'
|
|
426
|
+
}, contentContainerStyle],
|
|
427
|
+
showsVerticalScrollIndicator: showsVerticalScrollIndicator
|
|
428
|
+
// Let a tap on an input inside the sheet focus it on the FIRST tap
|
|
429
|
+
// even while the keyboard is already open (default 'never' would
|
|
430
|
+
// eat that tap just to dismiss the keyboard).
|
|
431
|
+
,
|
|
432
|
+
keyboardShouldPersistTaps: "handled",
|
|
433
|
+
animatedProps: animatedScrollProps,
|
|
434
|
+
alwaysBounceVertical: false,
|
|
435
|
+
overScrollMode: "always",
|
|
436
|
+
onScroll: useAnimatedScrollHandler(event => {
|
|
437
|
+
scrollY.value = event.contentOffset.y;
|
|
438
|
+
}),
|
|
439
|
+
scrollEventThrottle: 16,
|
|
440
|
+
children: children
|
|
441
|
+
})]
|
|
442
|
+
})
|
|
381
443
|
})
|
|
382
444
|
})
|
|
383
445
|
})
|
|
384
|
-
|
|
446
|
+
);
|
|
385
447
|
}
|
|
386
448
|
const styles = StyleSheet.create({
|
|
387
449
|
host: {
|
|
@@ -411,4 +473,6 @@ const styles = StyleSheet.create({
|
|
|
411
473
|
flex: 1
|
|
412
474
|
}
|
|
413
475
|
});
|
|
476
|
+
const Drawer = /*#__PURE__*/forwardRef(DrawerInner);
|
|
477
|
+
Drawer.displayName = 'Drawer';
|
|
414
478
|
export default Drawer;
|
|
@@ -170,18 +170,34 @@ export function Dropdown({
|
|
|
170
170
|
const shadowOffsetX = parseInt(getVariableByName('dropdown/shadow/offsetX', modes), 10) || 0;
|
|
171
171
|
const shadowOffsetY = parseInt(getVariableByName('dropdown/shadow/offsetY', modes), 10) || 4;
|
|
172
172
|
const shadowBlur = parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16;
|
|
173
|
-
|
|
173
|
+
|
|
174
|
+
// Shadow lives on the OUTER view, which must NOT set `overflow: 'hidden'`.
|
|
175
|
+
// On native, clipping a view also clips its shadow (iOS clips the layer
|
|
176
|
+
// shadow; Android clips the elevation shadow), so the soft popup shadow
|
|
177
|
+
// that renders fine on web (CSS box-shadow paints outside the box) would
|
|
178
|
+
// get cut off. The rounded-corner clipping is moved to a separate inner
|
|
179
|
+
// view below.
|
|
180
|
+
//
|
|
181
|
+
// The `boxShadow` style prop (RN 0.76+ / react-native-web) is used as the
|
|
182
|
+
// single source of truth so the designed color/offset/blur are honored on
|
|
183
|
+
// iOS, Android AND web. We intentionally do NOT also set the legacy
|
|
184
|
+
// `shadow*` / `elevation` props: on the new architecture (and web) those
|
|
185
|
+
// are translated into a box-shadow internally, so combining them with an
|
|
186
|
+
// explicit `boxShadow` would stack two shadows. Android's legacy
|
|
187
|
+
// `elevation` is also undesirable here because it ignores the shadow color
|
|
188
|
+
// and only paints a generic gray shadow.
|
|
189
|
+
const shadowStyle = {
|
|
174
190
|
backgroundColor: background,
|
|
191
|
+
borderRadius: radius,
|
|
192
|
+
boxShadow: `${shadowOffsetX}px ${shadowOffsetY}px ${shadowBlur}px 0px ${shadowColor}`
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Inner view carries the rounded corners + clipping so list/scroll content
|
|
196
|
+
// stays inside the radius without affecting the outer view's shadow.
|
|
197
|
+
const clipStyle = {
|
|
175
198
|
borderRadius: radius,
|
|
176
199
|
overflow: 'hidden',
|
|
177
|
-
|
|
178
|
-
shadowOffset: {
|
|
179
|
-
width: shadowOffsetX,
|
|
180
|
-
height: shadowOffsetY
|
|
181
|
-
},
|
|
182
|
-
shadowOpacity: 1,
|
|
183
|
-
shadowRadius: shadowBlur / 2,
|
|
184
|
-
elevation: 4
|
|
200
|
+
backgroundColor: background
|
|
185
201
|
};
|
|
186
202
|
const content = /*#__PURE__*/_jsx(View, {
|
|
187
203
|
style: {
|
|
@@ -190,17 +206,20 @@ export function Dropdown({
|
|
|
190
206
|
children: cloneChildrenWithModes(children, modes)
|
|
191
207
|
});
|
|
192
208
|
return /*#__PURE__*/_jsx(View, {
|
|
193
|
-
style: [
|
|
209
|
+
style: [shadowStyle, style],
|
|
194
210
|
accessibilityRole: "menu",
|
|
195
211
|
accessibilityLabel: accessibilityLabel || 'Dropdown menu',
|
|
196
|
-
children:
|
|
197
|
-
style:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
213
|
+
style: clipStyle,
|
|
214
|
+
children: maxHeight != null ? /*#__PURE__*/_jsx(ScrollView, {
|
|
215
|
+
style: {
|
|
216
|
+
maxHeight
|
|
217
|
+
},
|
|
218
|
+
showsVerticalScrollIndicator: true,
|
|
219
|
+
keyboardShouldPersistTaps: "handled",
|
|
220
|
+
children: content
|
|
221
|
+
}) : content
|
|
222
|
+
})
|
|
204
223
|
});
|
|
205
224
|
}
|
|
206
225
|
export default Dropdown;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useMemo, useRef } from 'react';
|
|
4
4
|
import { View, Text, Animated } from 'react-native';
|
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
6
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
7
|
import { useTokens } from '../../design-tokens/JFSThemeProvider';
|
|
7
8
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
@@ -183,6 +184,7 @@ function FullscreenModal({
|
|
|
183
184
|
heroMedia,
|
|
184
185
|
heroHeight = 420,
|
|
185
186
|
showClose = true,
|
|
187
|
+
closeOffsetY = 0,
|
|
186
188
|
onClose,
|
|
187
189
|
closeAccessibilityLabel = 'Close',
|
|
188
190
|
footer,
|
|
@@ -212,6 +214,24 @@ function FullscreenModal({
|
|
|
212
214
|
}), [globalModes, propModes]);
|
|
213
215
|
const rootGap = Number(getVariableByName('fullScreenModal/gap', modes)) || 16;
|
|
214
216
|
|
|
217
|
+
// Safe-area insets so the floating chrome clears the system bars: the close
|
|
218
|
+
// button drops below the status bar / notch, and the sticky footer keeps its
|
|
219
|
+
// designed bottom padding ON TOP of the bottom inset (home indicator /
|
|
220
|
+
// Android gesture or nav bar). On web — and anywhere without a
|
|
221
|
+
// SafeAreaProvider — every inset is 0, so the layout is unchanged.
|
|
222
|
+
const insets = useSafeAreaInsets();
|
|
223
|
+
const closeButtonInsetStyle = useMemo(() => ({
|
|
224
|
+
top: 12 + insets.top + closeOffsetY
|
|
225
|
+
}), [insets.top, closeOffsetY]);
|
|
226
|
+
// Extend (not replace) the footer's token bottom padding by the bottom inset
|
|
227
|
+
// so the action button never sits under the system navigation area.
|
|
228
|
+
const footerInsetStyle = useMemo(() => {
|
|
229
|
+
const base = Number(getVariableByName('actionFooter/padding/bottom', modes)) || 41;
|
|
230
|
+
return {
|
|
231
|
+
paddingBottom: base + insets.bottom
|
|
232
|
+
};
|
|
233
|
+
}, [modes, insets.bottom]);
|
|
234
|
+
|
|
215
235
|
// Drives the background's parallax-free sync with the scroll. The hero media
|
|
216
236
|
// lives at the ROOT (so it is never clipped to the content height and sits
|
|
217
237
|
// behind the transparent footer), but we translate it up by the exact scroll
|
|
@@ -311,12 +331,13 @@ function FullscreenModal({
|
|
|
311
331
|
})
|
|
312
332
|
}), footerContent ? /*#__PURE__*/_jsx(ActionFooter, {
|
|
313
333
|
modes: modes,
|
|
334
|
+
style: footerInsetStyle,
|
|
314
335
|
children: footerContent
|
|
315
336
|
}) : null, showClose ? /*#__PURE__*/_jsx(IconButton, {
|
|
316
337
|
iconName: "ic_close",
|
|
317
338
|
modes: modes,
|
|
318
339
|
accessibilityLabel: closeAccessibilityLabel,
|
|
319
|
-
style: closeButtonStyle,
|
|
340
|
+
style: [closeButtonStyle, closeButtonInsetStyle],
|
|
320
341
|
...(onClose ? {
|
|
321
342
|
onPress: onClose
|
|
322
343
|
} : {})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { View } from 'react-native';
|
|
5
5
|
import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
|
|
6
6
|
import Svg, { Path } from 'react-native-svg';
|
|
7
7
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
@@ -115,7 +115,11 @@ const useSegmentRotation = (clock, index, gravity, spreadMinRad, spreadMaxRad, s
|
|
|
115
115
|
};
|
|
116
116
|
}, [gravity, index, spreadMinRad, spreadMaxRad, spreadOutFrac]);
|
|
117
117
|
const fullSize = {
|
|
118
|
-
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
top: 0,
|
|
120
|
+
left: 0,
|
|
121
|
+
right: 0,
|
|
122
|
+
bottom: 0
|
|
119
123
|
};
|
|
120
124
|
function Spinner({
|
|
121
125
|
size = DEFAULT_SIZE,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { View, Text } from 'react-native';
|
|
5
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
+
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
+
import Avatar from '../Avatar/Avatar';
|
|
8
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
+
/**
|
|
10
|
+
* TestimonialsCard renders a compact, fixed-width card with a circular avatar,
|
|
11
|
+
* a bold title, and a body paragraph. It is typically used inside a horizontal
|
|
12
|
+
* carousel of customer testimonials.
|
|
13
|
+
*
|
|
14
|
+
* All styling values are resolved from Figma design tokens using the provided
|
|
15
|
+
* `modes`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <TestimonialsCard
|
|
20
|
+
* title="Aarav S."
|
|
21
|
+
* body="I was dreading renewing my car insurance, but JioFinance made it a breeze."
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function TestimonialsCard({
|
|
26
|
+
title = 'Title',
|
|
27
|
+
body = 'I was dreading renewing my car insurance, but JioFinance made it a breeze.',
|
|
28
|
+
modes = EMPTY_MODES,
|
|
29
|
+
style,
|
|
30
|
+
avatarProps,
|
|
31
|
+
accessibilityLabel
|
|
32
|
+
}) {
|
|
33
|
+
// Container tokens
|
|
34
|
+
const background = getVariableByName('testimonialsCard/background', modes) ?? '#ffffff';
|
|
35
|
+
const borderColor = getVariableByName('testimonialsCard/border/color', modes) ?? '#e8e8e8';
|
|
36
|
+
const radius = getVariableByName('testimonialsCard/radius', modes) ?? 8;
|
|
37
|
+
const gap = getVariableByName('testimonialsCard/gap', modes) ?? 8;
|
|
38
|
+
const paddingHorizontal = getVariableByName('testimonialsCard/padding/horizontal', modes) ?? 12;
|
|
39
|
+
const paddingVertical = getVariableByName('testimonialsCard/padding/vertical', modes) ?? 12;
|
|
40
|
+
|
|
41
|
+
// Title typography tokens
|
|
42
|
+
const titleColor = getVariableByName('testimonialsCard/subtitle/color', modes) ?? '#1a1c1f';
|
|
43
|
+
const titleFontSize = getVariableByName('testimonialsCard/title/fontSize', modes) ?? 14;
|
|
44
|
+
const titleFontFamily = getVariableByName('testimonialsCard/title/fontFamily', modes) ?? 'JioType Var';
|
|
45
|
+
const titleFontWeightRaw = getVariableByName('testimonialsCard/title/fontWeight', modes) ?? 700;
|
|
46
|
+
const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
|
|
47
|
+
|
|
48
|
+
// Body typography tokens
|
|
49
|
+
const bodyColor = getVariableByName('testimonialsCard/title/color', modes) ?? '#010101';
|
|
50
|
+
const bodyFontSize = getVariableByName('testimonialsCard/subtitle/fontSize', modes) ?? 12;
|
|
51
|
+
const bodyLineHeight = getVariableByName('testimonialsCard/subtitle/lineHeight', modes) ?? 16;
|
|
52
|
+
const bodyFontFamily = getVariableByName('testimonialsCard/subtitle/fontFamily', modes) ?? 'JioType Var';
|
|
53
|
+
const bodyFontWeightRaw = getVariableByName('testimonialsCard/subtitle/fontWeight', modes) ?? 400;
|
|
54
|
+
const bodyFontWeight = typeof bodyFontWeightRaw === 'number' ? bodyFontWeightRaw.toString() : bodyFontWeightRaw;
|
|
55
|
+
const containerStyle = {
|
|
56
|
+
width: 180,
|
|
57
|
+
backgroundColor: background,
|
|
58
|
+
borderColor: borderColor,
|
|
59
|
+
borderWidth: 1,
|
|
60
|
+
borderRadius: radius,
|
|
61
|
+
paddingHorizontal: paddingHorizontal,
|
|
62
|
+
paddingVertical: paddingVertical,
|
|
63
|
+
alignItems: 'flex-start',
|
|
64
|
+
gap: gap
|
|
65
|
+
};
|
|
66
|
+
const titleTextStyle = {
|
|
67
|
+
color: titleColor,
|
|
68
|
+
fontSize: titleFontSize,
|
|
69
|
+
lineHeight: bodyLineHeight,
|
|
70
|
+
fontFamily: titleFontFamily,
|
|
71
|
+
fontWeight: titleFontWeight,
|
|
72
|
+
width: '100%'
|
|
73
|
+
};
|
|
74
|
+
const bodyTextStyle = {
|
|
75
|
+
color: bodyColor,
|
|
76
|
+
fontSize: bodyFontSize,
|
|
77
|
+
lineHeight: bodyLineHeight,
|
|
78
|
+
fontFamily: bodyFontFamily,
|
|
79
|
+
fontWeight: bodyFontWeight,
|
|
80
|
+
width: '100%'
|
|
81
|
+
};
|
|
82
|
+
const avatarModes = {
|
|
83
|
+
...modes,
|
|
84
|
+
...(avatarProps?.modes || {})
|
|
85
|
+
};
|
|
86
|
+
const resolvedAccessibilityLabel = accessibilityLabel ?? `Testimonial${title ? ` from ${title}` : ''}${body ? `: ${body}` : ''}`;
|
|
87
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
88
|
+
style: [containerStyle, style],
|
|
89
|
+
accessibilityRole: "text",
|
|
90
|
+
accessibilityLabel: resolvedAccessibilityLabel,
|
|
91
|
+
children: [/*#__PURE__*/_jsx(Avatar, {
|
|
92
|
+
style: "Image",
|
|
93
|
+
modes: avatarModes,
|
|
94
|
+
...avatarProps
|
|
95
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
96
|
+
style: textContainerStyle,
|
|
97
|
+
children: [!!title && /*#__PURE__*/_jsx(Text, {
|
|
98
|
+
style: titleTextStyle,
|
|
99
|
+
accessibilityElementsHidden: true,
|
|
100
|
+
importantForAccessibility: "no-hide-descendants",
|
|
101
|
+
children: title
|
|
102
|
+
}), !!body && /*#__PURE__*/_jsx(Text, {
|
|
103
|
+
style: bodyTextStyle,
|
|
104
|
+
accessibilityElementsHidden: true,
|
|
105
|
+
importantForAccessibility: "no-hide-descendants",
|
|
106
|
+
children: body
|
|
107
|
+
})]
|
|
108
|
+
})]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const textContainerStyle = {
|
|
112
|
+
width: '100%',
|
|
113
|
+
alignItems: 'flex-start',
|
|
114
|
+
gap: 4
|
|
115
|
+
};
|
|
116
|
+
export default /*#__PURE__*/React.memo(TestimonialsCard);
|
|
@@ -131,6 +131,7 @@ export { default as StatItem } from './StatItem/StatItem';
|
|
|
131
131
|
export { default as StatGroup } from './StatGroup/StatGroup';
|
|
132
132
|
export { default as StrengthIndicator } from './StrengthIndicator/StrengthIndicator';
|
|
133
133
|
export { default as SummaryTile } from './SummaryTile/SummaryTile';
|
|
134
|
+
export { default as TestimonialsCard } from './TestimonialsCard/TestimonialsCard';
|
|
134
135
|
export { default as Text } from './Text/Text';
|
|
135
136
|
export { default as SegmentedControl } from './SegmentedControl/SegmentedControl';
|
|
136
137
|
export { default as Toggle } from './Toggle/Toggle';
|