jfs-components 0.1.30 → 0.1.32

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 (41) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
  3. package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
  4. package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
  5. package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
  6. package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
  7. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
  8. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
  9. package/lib/commonjs/components/index.js +35 -0
  10. package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
  11. package/lib/commonjs/icons/registry.js +1 -1
  12. package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
  13. package/lib/module/components/CompareTable/CompareTable.js +140 -84
  14. package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
  15. package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
  16. package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
  17. package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
  18. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
  19. package/lib/module/components/index.js +6 -1
  20. package/lib/module/design-tokens/figma-modes.generated.js +3 -0
  21. package/lib/module/icons/registry.js +1 -1
  22. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
  23. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
  24. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
  25. package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
  26. package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
  27. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
  28. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
  29. package/lib/typescript/src/components/index.d.ts +5 -0
  30. package/lib/typescript/src/icons/registry.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/src/components/CategoryCard/CategoryCard.tsx +404 -0
  33. package/src/components/CompareTable/CompareTable.tsx +201 -101
  34. package/src/components/ContentSheet/ContentSheet.tsx +40 -11
  35. package/src/components/CounterBadge/CounterBadge.tsx +95 -0
  36. package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
  37. package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
  38. package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
  39. package/src/components/index.ts +5 -0
  40. package/src/design-tokens/figma-modes.generated.ts +3 -0
  41. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo } from 'react';
4
+ import { Pressable, Text, 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, resolveTextLayout } from '../../utils/react-utils';
8
+ import { usePressableWebSupport } from '../../utils/web-platform-utils';
9
+ import Badge from '../Badge/Badge';
10
+ import Image from '../Image/Image';
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ const CATEGORY_CARD_STATE = 'Category Card State';
13
+ const BADGE_OVERLAP = 11;
14
+ const DEFAULT_CARD_WIDTH = 60;
15
+
16
+ // Default modes from the Figma CategoryCard component. Overridable via `modes`.
17
+ const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
18
+ AppearanceBrand: 'Tertiary',
19
+ 'Badge Size': 'Small',
20
+ Context4: 'Badge',
21
+ "Radius": "None"
22
+ });
23
+ const toNumber = (value, fallback) => {
24
+ if (typeof value === 'number') {
25
+ return Number.isFinite(value) ? value : fallback;
26
+ }
27
+ if (typeof value === 'string') {
28
+ const parsed = Number(value);
29
+ return Number.isFinite(parsed) ? parsed : fallback;
30
+ }
31
+ return fallback;
32
+ };
33
+ const toFontWeight = (value, fallback) => {
34
+ if (typeof value === 'number') return String(value);
35
+ if (typeof value === 'string') return value;
36
+ return fallback;
37
+ };
38
+ function resolveCategoryCardTokens(modes, active) {
39
+ const idleModes = {
40
+ ...modes,
41
+ [CATEGORY_CARD_STATE]: 'Idle'
42
+ };
43
+ const activeModes = {
44
+ ...modes,
45
+ [CATEGORY_CARD_STATE]: 'Active'
46
+ };
47
+ const resolvedModes = active ? activeModes : idleModes;
48
+ const gap = toNumber(getVariableByName('cardTab/gap', resolvedModes), 6);
49
+ const paddingHorizontal = toNumber(getVariableByName('cardTab/padding/horizontal', resolvedModes), 8);
50
+ const paddingVertical = toNumber(getVariableByName('cardTab/padding/vertical', resolvedModes), 8);
51
+ const radius = toNumber(getVariableByName('cardTab/radius', resolvedModes), 8);
52
+ const strokeWidth = toNumber(getVariableByName('cardTab/strokeWidth', resolvedModes), 1);
53
+ const strokeColor = getVariableByName('cardTab/stroke/color', resolvedModes) ?? '#f5f5f5';
54
+ const idleBackground = getVariableByName('cardTab/background/color', idleModes) ?? '#ffffff';
55
+ const activeBackground = getVariableByName('cardTab/background/color', activeModes) ?? '#f5f5f5';
56
+ const backgroundColor = active ? activeBackground : idleBackground;
57
+ const imageSize = toNumber(getVariableByName('cardTab/image/width', resolvedModes), 36);
58
+ const imageRadius = toNumber(getVariableByName('image/radius', resolvedModes), 0);
59
+ const fontFamily = getVariableByName('cardTab/label/fontFamily', resolvedModes) ?? 'JioType Var';
60
+ const fontSize = toNumber(getVariableByName('cardTab/label/fontSize', resolvedModes), 12);
61
+ const lineHeight = toNumber(getVariableByName('cardTab/label/lineHeight', resolvedModes), Math.round(fontSize * 1.3));
62
+ const fontWeight = toFontWeight(getVariableByName('cardTab/label/fontWeight', resolvedModes), '500');
63
+ const labelColor = getVariableByName('cardTab/label/color', resolvedModes) ?? '#000000';
64
+ return {
65
+ containerStyle: {
66
+ position: 'relative',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ backgroundColor,
70
+ borderColor: strokeColor,
71
+ borderWidth: strokeWidth,
72
+ borderStyle: 'solid',
73
+ borderRadius: radius,
74
+ paddingHorizontal,
75
+ paddingTop: paddingVertical,
76
+ paddingBottom: paddingVertical,
77
+ gap
78
+ },
79
+ labelStyle: {
80
+ color: labelColor,
81
+ fontFamily,
82
+ fontSize,
83
+ fontWeight,
84
+ lineHeight,
85
+ textAlign: 'center'
86
+ },
87
+ imageSize,
88
+ imageRadius
89
+ };
90
+ }
91
+ const pressedOverlayStyle = {
92
+ opacity: 0.85
93
+ };
94
+ const disabledOverlayStyle = {
95
+ opacity: 0.5
96
+ };
97
+
98
+ /**
99
+ * `CategoryCard` is a compact, tappable tile for category pickers and
100
+ * merchandising rails. It stacks a square image above a short label and can
101
+ * optionally show an overlapping badge at the top edge. Cards are typically
102
+ * laid out in a horizontal `ScrollView` or `HStack`.
103
+ *
104
+ * All visual values resolve through the `cardTab/*` design tokens with
105
+ * sensible Figma defaults so the card renders correctly out of the box.
106
+ *
107
+ * @component
108
+ * @param {CategoryCardProps} props
109
+ */
110
+ function CategoryCard({
111
+ label = 'Popular',
112
+ imageSource,
113
+ imageSlot,
114
+ badge,
115
+ active = false,
116
+ width = DEFAULT_CARD_WIDTH,
117
+ disableTruncation,
118
+ singleLine,
119
+ onPress,
120
+ disabled = false,
121
+ modes: propModes = EMPTY_MODES,
122
+ style,
123
+ labelStyle,
124
+ accessibilityLabel,
125
+ accessibilityHint,
126
+ accessibilityState,
127
+ webAccessibilityProps,
128
+ testID,
129
+ ...rest
130
+ }) {
131
+ const {
132
+ modes: globalModes
133
+ } = useTokens();
134
+ const modes = useMemo(() => ({
135
+ ...globalModes,
136
+ ...CATEGORY_CARD_DEFAULT_MODES,
137
+ ...propModes
138
+ }), [globalModes, propModes]);
139
+ const resolvedModes = useMemo(() => ({
140
+ ...modes,
141
+ [CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle'
142
+ }), [modes, active]);
143
+ const tokens = useMemo(() => resolveCategoryCardTokens(resolvedModes, active), [resolvedModes, active]);
144
+ const {
145
+ style: labelLayoutStyle,
146
+ ...labelTruncation
147
+ } = useMemo(() => resolveTextLayout({
148
+ disableTruncation,
149
+ singleLine,
150
+ numberOfLines: 1,
151
+ ellipsizeMode: 'tail'
152
+ }), [disableTruncation, singleLine]);
153
+ const processedImageSlot = useMemo(() => {
154
+ if (!imageSlot) return null;
155
+ const processed = cloneChildrenWithModes(imageSlot, resolvedModes);
156
+ return processed.length === 1 ? processed[0] : processed;
157
+ }, [imageSlot, resolvedModes]);
158
+ const imageNode = processedImageSlot ?? (imageSource !== undefined ? /*#__PURE__*/_jsx(Image, {
159
+ imageSource: imageSource,
160
+ width: tokens.imageSize,
161
+ height: tokens.imageSize,
162
+ ratio: 1,
163
+ resizeMode: "cover",
164
+ borderRadius: tokens.imageRadius,
165
+ accessibilityElementsHidden: true,
166
+ importantForAccessibility: "no"
167
+ }) : /*#__PURE__*/_jsx(Image, {
168
+ imageSource: "https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed",
169
+ width: tokens.imageSize,
170
+ height: tokens.imageSize,
171
+ ratio: 1,
172
+ resizeMode: "cover",
173
+ borderRadius: tokens.imageRadius,
174
+ accessibilityElementsHidden: true,
175
+ importantForAccessibility: "no"
176
+ }));
177
+ const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label);
178
+ const webProps = usePressableWebSupport({
179
+ restProps: rest,
180
+ onPress: disabled ? undefined : onPress,
181
+ disabled,
182
+ accessibilityLabel: a11yLabel,
183
+ webAccessibilityProps
184
+ });
185
+ const badgeNode = badge ? /*#__PURE__*/_jsx(View, {
186
+ style: BADGE_ANCHOR,
187
+ pointerEvents: "none",
188
+ children: /*#__PURE__*/_jsx(Badge, {
189
+ label: badge,
190
+ modes: resolvedModes,
191
+ style: BADGE_CENTER_STYLE
192
+ })
193
+ }) : null;
194
+
195
+ // Truncation needs a width bound so the ellipsis engages. `singleLine`
196
+ // must NOT fill the content box — otherwise the text is pinned to the left
197
+ // padding edge and only grows rightward. Intrinsic width + centered
198
+ // alignment lets overflow expand equally on both sides.
199
+ const labelWidthStyle = singleLine ? LABEL_OVERFLOW_CENTER_STYLE : LABEL_FILL_STYLE;
200
+ const pressableStyle = useCallback(({
201
+ pressed
202
+ }) => [tokens.containerStyle, {
203
+ width
204
+ }, badge ? {
205
+ marginTop: BADGE_OVERLAP
206
+ } : null, disabled ? disabledOverlayStyle : null, pressed && !disabled ? pressedOverlayStyle : null, style], [tokens.containerStyle, width, badge, disabled, style]);
207
+ return /*#__PURE__*/_jsxs(Pressable, {
208
+ accessibilityRole: "button",
209
+ accessibilityLabel: a11yLabel,
210
+ accessibilityHint: accessibilityHint,
211
+ accessibilityState: {
212
+ disabled,
213
+ selected: active,
214
+ ...accessibilityState
215
+ },
216
+ onPress: onPress,
217
+ disabled: disabled,
218
+ style: pressableStyle,
219
+ testID: testID,
220
+ ...webProps,
221
+ children: [badgeNode, imageNode, /*#__PURE__*/_jsx(Text, {
222
+ style: [tokens.labelStyle, labelWidthStyle, labelLayoutStyle, labelStyle],
223
+ ...labelTruncation,
224
+ accessibilityElementsHidden: true,
225
+ importantForAccessibility: "no",
226
+ children: label
227
+ })]
228
+ });
229
+ }
230
+ const BADGE_ANCHOR = {
231
+ position: 'absolute',
232
+ top: -BADGE_OVERLAP,
233
+ left: 0,
234
+ right: 0,
235
+ alignItems: 'center',
236
+ justifyContent: 'center',
237
+ zIndex: 1
238
+ };
239
+ const BADGE_CENTER_STYLE = {
240
+ alignSelf: 'center'
241
+ };
242
+
243
+ /** Lets the label fill the card width so the one-line ellipsis has a bound. */
244
+ const LABEL_FILL_STYLE = {
245
+ width: '100%',
246
+ alignSelf: 'stretch'
247
+ };
248
+
249
+ /**
250
+ * Intrinsic width, horizontally centred. When the label is wider than the
251
+ * card it overflows left and right equally instead of growing only rightward
252
+ * from the padded content edge.
253
+ */
254
+ const LABEL_OVERFLOW_CENTER_STYLE = {
255
+ alignSelf: 'center',
256
+ textAlign: 'center'
257
+ };
258
+ export default /*#__PURE__*/React.memo(CategoryCard);
@@ -107,7 +107,9 @@ function CompareTable({
107
107
  modes = EMPTY_MODES,
108
108
  style,
109
109
  disableTruncation,
110
- columnWidth
110
+ columnWidth,
111
+ stickyHeaders = true,
112
+ stickyHeaderLabel = true
111
113
  }) {
112
114
  // --- selection card tokens ------------------------------------------------
113
115
  const cardBg = getVariableByName('selectionCard/background/color', modes) ?? '#ffffff';
@@ -179,12 +181,12 @@ function CompareTable({
179
181
  // Stable per-section ref-callback instances so React doesn't detach/reattach
180
182
  // on every render (which would churn the map and re-fire scrollTo).
181
183
  const bodyRefCallbacks = useRef(new Map());
182
- /** Last offset we synced everyone to. Echoed onScroll events reporting this
183
- * same value are ignoredthis is what prevents feedback loops. */
184
+ /** Tracks which ScrollView the user is currently dragging. Only the
185
+ * actively-dragged ScrollView drives horizontal sync programmatic
186
+ * scrollTo echoes from synced peers are ignored, eliminating jitter. */
187
+ const draggingSource = useRef(null);
184
188
  const lastSyncX = useRef(0);
185
- const applySync = useCallback((x, source) => {
186
- if (Math.abs(x - lastSyncX.current) < 0.5) return;
187
- lastSyncX.current = x;
189
+ const syncOthers = useCallback((source, x) => {
188
190
  if (source !== 'cards' && cardsScrollRef.current) {
189
191
  cardsScrollRef.current.scrollTo({
190
192
  x,
@@ -200,12 +202,37 @@ function CompareTable({
200
202
  }
201
203
  });
202
204
  }, []);
203
- const onCardsScroll = useCallback(e => {
204
- applySync(e.nativeEvent.contentOffset.x, 'cards');
205
- }, [applySync]);
206
- const onBodyScroll = useCallback(idx => e => {
207
- applySync(e.nativeEvent.contentOffset.x, idx);
208
- }, [applySync]);
205
+ const onScrollBeginDrag = useCallback(source => () => {
206
+ draggingSource.current = source;
207
+ }, []);
208
+ const onScrollEndDrag = useCallback(source => e => {
209
+ const x = Math.round(e.nativeEvent.contentOffset.x);
210
+ if (x === lastSyncX.current) return;
211
+ lastSyncX.current = x;
212
+ syncOthers(source, x);
213
+ }, [syncOthers]);
214
+ const onMomentumScrollEnd = useCallback(source => e => {
215
+ if (draggingSource.current === source) {
216
+ draggingSource.current = null;
217
+ }
218
+ const x = Math.round(e.nativeEvent.contentOffset.x);
219
+ if (x === lastSyncX.current) return;
220
+ lastSyncX.current = x;
221
+ syncOthers(source, x);
222
+ }, [syncOthers]);
223
+
224
+ /** Real-time scroll sync: only the actively-dragged ScrollView drives
225
+ * sync on native — echoed onScroll events from programmatic scrollTo
226
+ * on other ScrollViews are ignored because draggingSource won't match.
227
+ * On web, onScrollBeginDrag never fires for mouse/trackpad, so skip
228
+ * the guard and rely on the lastSyncX rounded-value check instead. */
229
+ const onScroll = useCallback(source => e => {
230
+ if (Platform.OS !== 'web' && draggingSource.current !== source) return;
231
+ const x = Math.round(e.nativeEvent.contentOffset.x);
232
+ if (x === lastSyncX.current) return;
233
+ lastSyncX.current = x;
234
+ syncOthers(source, x);
235
+ }, [syncOthers]);
209
236
  const getBodyRefCallback = useCallback(idx => {
210
237
  let cb = bodyRefCallbacks.current.get(idx);
211
238
  if (!cb) {
@@ -436,55 +463,45 @@ function CompareTable({
436
463
  })
437
464
  })]
438
465
  });
439
-
440
- // Render a single section in fixed-width scroll mode. Unlike `renderTable`
441
- // (used in flex mode), the gray header row is rendered full-width OUTSIDE
442
- // the body's horizontal ScrollView so it stays put horizontally (it does
443
- // not slide with the cells) while still scrolling vertically with the table.
444
- // The body rows live inside a horizontal ScrollView that is scroll-synced
445
- // with the cards row and every other section's body.
446
- const renderSection = (section, index) => /*#__PURE__*/_jsxs(Accordion, {
447
- title: section.title,
448
- defaultExpanded: section.defaultExpanded ?? index === 0,
449
- modes: modes,
450
- children: [section.header != null && /*#__PURE__*/_jsx(View, {
466
+ const renderHeaderContent = section => section.header != null ? /*#__PURE__*/_jsx(View, {
467
+ style: {
468
+ backgroundColor: headerBg,
469
+ paddingHorizontal: headerPaddingH,
470
+ paddingVertical: headerPaddingV,
471
+ borderBottomWidth: cellBorderSize,
472
+ borderBottomColor: cellBorderColor
473
+ },
474
+ children: /*#__PURE__*/_jsx(Text, {
475
+ style: headerTextStyle,
476
+ children: section.header
477
+ })
478
+ }) : null;
479
+ const renderBodyContent = (section, index) => /*#__PURE__*/_jsx(ScrollView, {
480
+ ref: getBodyRefCallback(index),
481
+ horizontal: true,
482
+ showsHorizontalScrollIndicator: false,
483
+ onScroll: onScroll(index),
484
+ onScrollBeginDrag: onScrollBeginDrag(index),
485
+ onScrollEndDrag: onScrollEndDrag(index),
486
+ onMomentumScrollEnd: onMomentumScrollEnd(index),
487
+ children: /*#__PURE__*/_jsx(View, {
451
488
  style: {
452
- width: '100%',
453
- backgroundColor: headerBg,
454
- paddingHorizontal: headerPaddingH,
455
- paddingVertical: headerPaddingV,
456
- borderBottomWidth: cellBorderSize,
457
- borderBottomColor: cellBorderColor
489
+ width: contentWidth
458
490
  },
459
- children: /*#__PURE__*/_jsx(Text, {
460
- style: headerTextStyle,
461
- children: section.header
462
- })
463
- }), /*#__PURE__*/_jsx(ScrollView, {
464
- ref: getBodyRefCallback(index),
465
- horizontal: true,
466
- showsHorizontalScrollIndicator: false,
467
- scrollEventThrottle: 16,
468
- onScroll: onBodyScroll(index),
469
- children: /*#__PURE__*/_jsx(View, {
470
- style: {
471
- width: contentWidth
472
- },
473
- children: section.rows.map((row, rowIndex) => {
474
- const isLastRow = rowIndex === section.rows.length - 1;
475
- return /*#__PURE__*/_jsxs(View, {
476
- style: {
477
- flexDirection: 'row',
478
- width: '100%'
479
- },
480
- children: [Array.from({
481
- length: columnCount
482
- }).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
483
- }, row.key ?? rowIndex);
484
- })
491
+ children: section.rows.map((row, rowIndex) => {
492
+ const isLastRow = rowIndex === section.rows.length - 1;
493
+ return /*#__PURE__*/_jsxs(View, {
494
+ style: {
495
+ flexDirection: 'row',
496
+ width: '100%'
497
+ },
498
+ children: [Array.from({
499
+ length: columnCount
500
+ }).map((_, colIndex) => renderTableCell(renderCellContent(row.values?.[colIndex], `${rowIndex}-${colIndex}`), colIndex, isLastRow, colIndex)), showAddCard && renderTableCell(null, gridColumnCount - 1, isLastRow, '__add_spacer__')]
501
+ }, row.key ?? rowIndex);
485
502
  })
486
- })]
487
- }, section.key ?? section.title ?? index);
503
+ })
504
+ });
488
505
  const outerStyle = {
489
506
  width: '100%',
490
507
  backgroundColor: cardBg,
@@ -518,43 +535,82 @@ function CompareTable({
518
535
  }
519
536
 
520
537
  // --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
521
- // The cards row (index 0) is a sticky header of the vertical ScrollView so
522
- // it pins to the top while the sections scroll beneath. The cards row is
523
- // itself a horizontal ScrollView, synced with every section body. For the
524
- // vertical scroll (and thus the sticky behaviour) to engage, the host must
525
- // give CompareTable a bounded height (e.g. a sized container or flex:1).
538
+ // Build children and stickyHeaderIndices for the vertical ScrollView.
539
+ // When stickyHeaders is on, section headers are injected as direct children
540
+ // so React Native's stickyHeaderIndices can pin them with stacking.
541
+ const scrollChildren = [];
542
+ const stickyIndices = [];
543
+ if (showCardsRow) {
544
+ stickyIndices.push(scrollChildren.length);
545
+ scrollChildren.push(/*#__PURE__*/_jsx(ScrollView, {
546
+ ref: cardsScrollRef,
547
+ horizontal: true,
548
+ showsHorizontalScrollIndicator: false,
549
+ scrollEventThrottle: 16,
550
+ onScroll: onScroll('cards'),
551
+ onScrollBeginDrag: onScrollBeginDrag('cards'),
552
+ onScrollEndDrag: onScrollEndDrag('cards'),
553
+ onMomentumScrollEnd: onMomentumScrollEnd('cards'),
554
+ children: /*#__PURE__*/_jsxs(View, {
555
+ style: {
556
+ flexDirection: 'row',
557
+ width: contentWidth
558
+ },
559
+ children: [columns.map(renderCard), showAddCard && renderAddCard()]
560
+ })
561
+ }, "cards-row"));
562
+ }
563
+ sections.forEach((section, index) => {
564
+ const sectionKey = section.key ?? section.title ?? index;
565
+ const headerContent = renderHeaderContent(section);
566
+ if (stickyHeaders && headerContent != null) {
567
+ stickyIndices.push(scrollChildren.length);
568
+ scrollChildren.push(/*#__PURE__*/_jsx(View, {
569
+ style: {
570
+ width: '100%'
571
+ },
572
+ children: stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
573
+ horizontal: true,
574
+ showsHorizontalScrollIndicator: false,
575
+ children: /*#__PURE__*/_jsx(View, {
576
+ style: {
577
+ width: contentWidth
578
+ },
579
+ children: headerContent
580
+ })
581
+ })
582
+ }, `sticky-header-${sectionKey}`));
583
+ }
584
+ scrollChildren.push(/*#__PURE__*/_jsxs(Accordion, {
585
+ title: section.title,
586
+ defaultExpanded: section.defaultExpanded ?? index === 0,
587
+ modes: modes,
588
+ children: [!stickyHeaders && headerContent != null && (stickyHeaderLabel ? headerContent : /*#__PURE__*/_jsx(ScrollView, {
589
+ horizontal: true,
590
+ showsHorizontalScrollIndicator: false,
591
+ children: /*#__PURE__*/_jsx(View, {
592
+ style: {
593
+ width: contentWidth
594
+ },
595
+ children: headerContent
596
+ })
597
+ })), renderBodyContent(section, index)]
598
+ }, sectionKey));
599
+ });
526
600
  return /*#__PURE__*/_jsx(View, {
527
601
  style: [outerStyle, style, {
528
602
  flexDirection: 'column',
529
603
  flex: 1
530
604
  }],
531
- children: /*#__PURE__*/_jsxs(ScrollView, {
605
+ children: /*#__PURE__*/_jsx(ScrollView, {
532
606
  style: {
533
607
  width: '100%',
534
608
  flex: 1
535
609
  },
536
- stickyHeaderIndices: showCardsRow ? [0] : [],
610
+ stickyHeaderIndices: stickyIndices,
537
611
  showsVerticalScrollIndicator: false,
538
612
  directionalLockEnabled: true,
539
- children: [showCardsRow && /*#__PURE__*/_jsx(ScrollView, {
540
- ref: cardsScrollRef,
541
- horizontal: true,
542
- showsHorizontalScrollIndicator: false,
543
- scrollEventThrottle: 16,
544
- onScroll: onCardsScroll,
545
- children: /*#__PURE__*/_jsxs(View, {
546
- style: {
547
- flexDirection: 'row',
548
- width: contentWidth
549
- },
550
- children: [columns.map(renderCard), showAddCard && renderAddCard()]
551
- })
552
- }), /*#__PURE__*/_jsx(View, {
553
- style: {
554
- width: '100%'
555
- },
556
- children: sections.map(renderSection)
557
- })]
613
+ children: scrollChildren
558
614
  })
559
615
  });
560
616
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useContext, useEffect, useMemo } from 'react';
4
- import { Platform, Text, View, useWindowDimensions } from 'react-native';
4
+ import { Platform, ScrollView, Text, View, useWindowDimensions } from 'react-native';
5
5
  import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
6
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
@@ -104,6 +104,7 @@ function ContentSheet({
104
104
  safeAreaBottom = true,
105
105
  pinToBottom = true,
106
106
  visible = true,
107
+ maxHeightPercent = 0.7,
107
108
  style,
108
109
  ...rest
109
110
  }) {
@@ -115,9 +116,6 @@ function ContentSheet({
115
116
  const resolved = useMemo(() => resolveContentSheetStyle(modes), [modes]);
116
117
  const processedChildren = useMemo(() => cloneChildrenWithModes(flattenChildren(children), modes), [children, modes]);
117
118
  const paddingBottom = resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0);
118
- const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
119
- paddingBottom
120
- }, style], [pinToBottom, resolved.container, paddingBottom, style]);
121
119
 
122
120
  // Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
123
121
  // sheet is bottom-anchored, so a positive `translateY` equal to the window
@@ -127,6 +125,11 @@ function ContentSheet({
127
125
  const {
128
126
  height: windowHeight
129
127
  } = useWindowDimensions();
128
+ const maxHeight = windowHeight * maxHeightPercent;
129
+ const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
130
+ maxHeight,
131
+ paddingBottom
132
+ }, style], [pinToBottom, resolved.container, maxHeight, paddingBottom, style]);
130
133
  const entrance = useSharedValue(windowHeight);
131
134
  useEffect(() => {
132
135
  entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG);
@@ -150,6 +153,7 @@ function ContentSheet({
150
153
  style: containerStyle,
151
154
  spacing: keyboardSpacing,
152
155
  entrance: entrance,
156
+ maxHeight: maxHeight,
153
157
  ...rest,
154
158
  children: [header, processedChildren]
155
159
  });
@@ -157,6 +161,7 @@ function ContentSheet({
157
161
  return /*#__PURE__*/_jsxs(AnimatedSheet, {
158
162
  style: containerStyle,
159
163
  entrance: entrance,
164
+ maxHeight: maxHeight,
160
165
  ...rest,
161
166
  children: [header, processedChildren]
162
167
  });
@@ -173,6 +178,7 @@ function KeyboardAwareSheet({
173
178
  style,
174
179
  spacing,
175
180
  entrance,
181
+ maxHeight,
176
182
  children,
177
183
  ...rest
178
184
  }) {
@@ -189,7 +195,15 @@ function KeyboardAwareSheet({
189
195
  return /*#__PURE__*/_jsx(Animated.View, {
190
196
  style: [style, animatedStyle],
191
197
  ...rest,
192
- children: children
198
+ children: /*#__PURE__*/_jsx(ScrollView, {
199
+ style: {
200
+ maxHeight
201
+ },
202
+ bounces: false,
203
+ showsVerticalScrollIndicator: false,
204
+ keyboardShouldPersistTaps: "handled",
205
+ children: children
206
+ })
193
207
  });
194
208
  }
195
209
 
@@ -201,6 +215,7 @@ function KeyboardAwareSheet({
201
215
  function AnimatedSheet({
202
216
  style,
203
217
  entrance,
218
+ maxHeight,
204
219
  children,
205
220
  ...rest
206
221
  }) {
@@ -214,7 +229,15 @@ function AnimatedSheet({
214
229
  return /*#__PURE__*/_jsx(Animated.View, {
215
230
  style: [style, animatedStyle],
216
231
  ...rest,
217
- children: children
232
+ children: /*#__PURE__*/_jsx(ScrollView, {
233
+ style: {
234
+ maxHeight
235
+ },
236
+ bounces: false,
237
+ showsVerticalScrollIndicator: false,
238
+ keyboardShouldPersistTaps: "handled",
239
+ children: children
240
+ })
218
241
  });
219
242
  }
220
243
  export default ContentSheet;