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.
Files changed (31) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/commonjs/components/Drawer/Drawer.js +146 -82
  3. package/lib/commonjs/components/Dropdown/Dropdown.js +37 -18
  4. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +22 -1
  5. package/lib/commonjs/components/Spinner/Spinner.js +5 -1
  6. package/lib/commonjs/components/TestimonialsCard/TestimonialsCard.js +121 -0
  7. package/lib/commonjs/components/index.js +7 -0
  8. package/lib/commonjs/icons/registry.js +1 -1
  9. package/lib/commonjs/skeleton/Skeleton.js +10 -2
  10. package/lib/module/components/Drawer/Drawer.js +148 -84
  11. package/lib/module/components/Dropdown/Dropdown.js +37 -18
  12. package/lib/module/components/FullscreenModal/FullscreenModal.js +22 -1
  13. package/lib/module/components/Spinner/Spinner.js +6 -2
  14. package/lib/module/components/TestimonialsCard/TestimonialsCard.js +116 -0
  15. package/lib/module/components/index.js +1 -0
  16. package/lib/module/icons/registry.js +1 -1
  17. package/lib/module/skeleton/Skeleton.js +11 -3
  18. package/lib/typescript/src/components/Drawer/Drawer.d.ts +23 -4
  19. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +7 -1
  20. package/lib/typescript/src/components/TestimonialsCard/TestimonialsCard.d.ts +51 -0
  21. package/lib/typescript/src/components/index.d.ts +2 -1
  22. package/lib/typescript/src/icons/registry.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/src/components/Drawer/Drawer.tsx +94 -15
  25. package/src/components/Dropdown/Dropdown.tsx +38 -18
  26. package/src/components/FullscreenModal/FullscreenModal.tsx +29 -2
  27. package/src/components/Spinner/Spinner.tsx +2 -2
  28. package/src/components/TestimonialsCard/TestimonialsCard.tsx +162 -0
  29. package/src/components/index.ts +2 -1
  30. package/src/icons/registry.ts +1 -1
  31. 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 Drawer({
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 state for accessibility/logic if needed
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(prev => {
134
- if (prev !== newMode) {
135
- onStateChange?.(newMode);
136
- }
137
- return newMode;
138
- });
139
- }, [onStateChange]);
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 /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureHandlerRootView, {
313
- style: [styles.host, style],
314
- pointerEvents: "box-none",
315
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
316
- gesture: gesture,
317
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
318
- style: [styles.sheet, {
319
- // Constraint the height strictly to the expanded height
320
- // This ensures the ScrollView has a finite frame to scroll within
321
- height: expandedHeight,
322
- backgroundColor,
323
- borderTopLeftRadius: radius,
324
- borderTopRightRadius: radius
325
- }, shadowStyle, sheetStyle, animatedStyle],
326
- accessible: true,
327
- ...(_reactNative.Platform.OS === 'web' ? {
328
- accessibilityRole: 'dialog'
329
- } : undefined),
330
- accessibilityLabel: undefined,
331
- accessibilityHint: accessibilityHint || 'Swipe up to expand, swipe down to collapse',
332
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
333
- style: [styles.sheetInner, {
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
- paddingLeft,
337
- paddingRight,
338
- paddingBottom,
339
- rowGap: drawerGap
340
- }],
341
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
342
- style: [styles.handleArea, !title && !header && {
343
- paddingBottom: 0
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
- backgroundColor: handleColor,
348
- width: handleWidth,
349
- height: handleHeight,
350
- borderRadius: handleRadius
351
- }]
352
- })
353
- }), header, title && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
354
- style: [{
355
- color: titleColor,
356
- fontSize: titleSize,
357
- fontWeight: titleWeight,
358
- lineHeight: titleLineHeight,
359
- marginBottom: titlePaddingBottom
360
- }],
361
- children: title
362
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedScrollView, {
363
- ref: scrollRef,
364
- style: [styles.content, contentStyle],
365
- contentContainerStyle: [{
366
- paddingBottom: paddingBottom + bottomInset,
367
- gap: drawerGap,
368
- flexDirection: 'column',
369
- alignItems: 'stretch'
370
- }, contentContainerStyle],
371
- showsVerticalScrollIndicator: showsVerticalScrollIndicator
372
- // Let a tap on an input inside the sheet focus it on the FIRST tap
373
- // even while the keyboard is already open (default 'never' would
374
- // eat that tap just to dismiss the keyboard).
375
- ,
376
- keyboardShouldPersistTaps: "handled",
377
- animatedProps: animatedScrollProps,
378
- alwaysBounceVertical: false,
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
- const containerStyle = {
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
- shadowColor,
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: [containerStyle, style],
217
+ style: [shadowStyle, style],
202
218
  accessibilityRole: "menu",
203
219
  accessibilityLabel: accessibilityLabel || 'Dropdown menu',
204
- children: maxHeight != null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
205
- style: {
206
- maxHeight
207
- },
208
- showsVerticalScrollIndicator: true,
209
- keyboardShouldPersistTaps: "handled",
210
- children: content
211
- }) : content
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
- ..._reactNative.StyleSheet.absoluteFillObject
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"));