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