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.
- package/CHANGELOG.md +13 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -163,6 +163,21 @@ export type CompareTableProps = {
|
|
|
163
163
|
* no scrolling). @default undefined
|
|
164
164
|
*/
|
|
165
165
|
columnWidth?: number;
|
|
166
|
+
/**
|
|
167
|
+
* When `true` (default), each section's header row (the gray header label)
|
|
168
|
+
* sticks to the top of the viewport when scrolling vertically, with multiple
|
|
169
|
+
* sticky headers stacking up as each reaches the top. Only applies in
|
|
170
|
+
* fixed-width scroll mode (`columnWidth` is set).
|
|
171
|
+
* @default true
|
|
172
|
+
*/
|
|
173
|
+
stickyHeaders?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* When `true` (default), the section header label stays fixed on the left
|
|
176
|
+
* when scrolling horizontally. Only applies in fixed-width scroll mode
|
|
177
|
+
* (`columnWidth` is set).
|
|
178
|
+
* @default true
|
|
179
|
+
*/
|
|
180
|
+
stickyHeaderLabel?: boolean;
|
|
166
181
|
};
|
|
167
182
|
|
|
168
183
|
const DEFAULT_COLUMNS: CompareTableColumn[] = [
|
|
@@ -205,6 +220,8 @@ function CompareTable({
|
|
|
205
220
|
style,
|
|
206
221
|
disableTruncation,
|
|
207
222
|
columnWidth,
|
|
223
|
+
stickyHeaders = true,
|
|
224
|
+
stickyHeaderLabel = true,
|
|
208
225
|
}: CompareTableProps) {
|
|
209
226
|
// --- selection card tokens ------------------------------------------------
|
|
210
227
|
const cardBg = (getVariableByName('selectionCard/background/color', modes) as string) ?? '#ffffff'
|
|
@@ -278,13 +295,13 @@ function CompareTable({
|
|
|
278
295
|
// Stable per-section ref-callback instances so React doesn't detach/reattach
|
|
279
296
|
// on every render (which would churn the map and re-fire scrollTo).
|
|
280
297
|
const bodyRefCallbacks = useRef<Map<number, (r: ScrollView | null) => void>>(new Map())
|
|
281
|
-
/**
|
|
282
|
-
*
|
|
298
|
+
/** Tracks which ScrollView the user is currently dragging. Only the
|
|
299
|
+
* actively-dragged ScrollView drives horizontal sync — programmatic
|
|
300
|
+
* scrollTo echoes from synced peers are ignored, eliminating jitter. */
|
|
301
|
+
const draggingSource = useRef<'cards' | number | null>(null)
|
|
283
302
|
const lastSyncX = useRef(0)
|
|
284
303
|
|
|
285
|
-
const
|
|
286
|
-
if (Math.abs(x - lastSyncX.current) < 0.5) return
|
|
287
|
-
lastSyncX.current = x
|
|
304
|
+
const syncOthers = useCallback((source: 'cards' | number, x: number) => {
|
|
288
305
|
if (source !== 'cards' && cardsScrollRef.current) {
|
|
289
306
|
cardsScrollRef.current.scrollTo({ x, animated: false })
|
|
290
307
|
}
|
|
@@ -295,18 +312,54 @@ function CompareTable({
|
|
|
295
312
|
})
|
|
296
313
|
}, [])
|
|
297
314
|
|
|
298
|
-
const
|
|
299
|
-
(
|
|
300
|
-
|
|
315
|
+
const onScrollBeginDrag = useCallback(
|
|
316
|
+
(source: 'cards' | number) => () => {
|
|
317
|
+
draggingSource.current = source
|
|
301
318
|
},
|
|
302
|
-
[
|
|
319
|
+
[],
|
|
303
320
|
)
|
|
304
321
|
|
|
305
|
-
const
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
const onScrollEndDrag = useCallback(
|
|
323
|
+
(source: 'cards' | number) =>
|
|
324
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
325
|
+
const x = Math.round(e.nativeEvent.contentOffset.x)
|
|
326
|
+
if (x === lastSyncX.current) return
|
|
327
|
+
lastSyncX.current = x
|
|
328
|
+
syncOthers(source, x)
|
|
329
|
+
},
|
|
330
|
+
[syncOthers],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
const onMomentumScrollEnd = useCallback(
|
|
334
|
+
(source: 'cards' | number) =>
|
|
335
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
336
|
+
if (draggingSource.current === source) {
|
|
337
|
+
draggingSource.current = null
|
|
338
|
+
}
|
|
339
|
+
const x = Math.round(e.nativeEvent.contentOffset.x)
|
|
340
|
+
if (x === lastSyncX.current) return
|
|
341
|
+
lastSyncX.current = x
|
|
342
|
+
syncOthers(source, x)
|
|
343
|
+
},
|
|
344
|
+
[syncOthers],
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
/** Real-time scroll sync: only the actively-dragged ScrollView drives
|
|
348
|
+
* sync on native — echoed onScroll events from programmatic scrollTo
|
|
349
|
+
* on other ScrollViews are ignored because draggingSource won't match.
|
|
350
|
+
* On web, onScrollBeginDrag never fires for mouse/trackpad, so skip
|
|
351
|
+
* the guard and rely on the lastSyncX rounded-value check instead. */
|
|
352
|
+
const onScroll = useCallback(
|
|
353
|
+
(source: 'cards' | number) =>
|
|
354
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
355
|
+
if (Platform.OS !== 'web' && draggingSource.current !== source) return
|
|
356
|
+
|
|
357
|
+
const x = Math.round(e.nativeEvent.contentOffset.x)
|
|
358
|
+
if (x === lastSyncX.current) return
|
|
359
|
+
lastSyncX.current = x
|
|
360
|
+
syncOthers(source, x)
|
|
361
|
+
},
|
|
362
|
+
[syncOthers],
|
|
310
363
|
)
|
|
311
364
|
|
|
312
365
|
const getBodyRefCallback = useCallback((idx: number) => {
|
|
@@ -576,72 +629,62 @@ function CompareTable({
|
|
|
576
629
|
</Table>
|
|
577
630
|
)
|
|
578
631
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
defaultExpanded={section.defaultExpanded ?? index === 0}
|
|
590
|
-
modes={modes}
|
|
591
|
-
>
|
|
592
|
-
{section.header != null && (
|
|
593
|
-
<View
|
|
594
|
-
style={{
|
|
595
|
-
width: '100%',
|
|
596
|
-
backgroundColor: headerBg,
|
|
597
|
-
paddingHorizontal: headerPaddingH,
|
|
598
|
-
paddingVertical: headerPaddingV,
|
|
599
|
-
borderBottomWidth: cellBorderSize,
|
|
600
|
-
borderBottomColor: cellBorderColor,
|
|
601
|
-
}}
|
|
602
|
-
>
|
|
603
|
-
<Text style={headerTextStyle}>{section.header}</Text>
|
|
604
|
-
</View>
|
|
605
|
-
)}
|
|
606
|
-
<ScrollView
|
|
607
|
-
ref={getBodyRefCallback(index)}
|
|
608
|
-
horizontal
|
|
609
|
-
showsHorizontalScrollIndicator={false}
|
|
610
|
-
scrollEventThrottle={16}
|
|
611
|
-
onScroll={onBodyScroll(index)}
|
|
632
|
+
const renderHeaderContent = (section: CompareTableSection) =>
|
|
633
|
+
section.header != null ? (
|
|
634
|
+
<View
|
|
635
|
+
style={{
|
|
636
|
+
backgroundColor: headerBg,
|
|
637
|
+
paddingHorizontal: headerPaddingH,
|
|
638
|
+
paddingVertical: headerPaddingV,
|
|
639
|
+
borderBottomWidth: cellBorderSize,
|
|
640
|
+
borderBottomColor: cellBorderColor,
|
|
641
|
+
}}
|
|
612
642
|
>
|
|
613
|
-
<
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
643
|
+
<Text style={headerTextStyle}>{section.header}</Text>
|
|
644
|
+
</View>
|
|
645
|
+
) : null
|
|
646
|
+
|
|
647
|
+
const renderBodyContent = (section: CompareTableSection, index: number) => (
|
|
648
|
+
<ScrollView
|
|
649
|
+
ref={getBodyRefCallback(index)}
|
|
650
|
+
horizontal
|
|
651
|
+
showsHorizontalScrollIndicator={false}
|
|
652
|
+
onScroll={onScroll(index)}
|
|
653
|
+
onScrollBeginDrag={onScrollBeginDrag(index)}
|
|
654
|
+
onScrollEndDrag={onScrollEndDrag(index)}
|
|
655
|
+
onMomentumScrollEnd={onMomentumScrollEnd(index)}
|
|
656
|
+
>
|
|
657
|
+
<View style={{ width: contentWidth }}>
|
|
658
|
+
{section.rows.map((row, rowIndex) => {
|
|
659
|
+
const isLastRow = rowIndex === section.rows.length - 1
|
|
660
|
+
return (
|
|
661
|
+
<View
|
|
662
|
+
key={row.key ?? rowIndex}
|
|
663
|
+
style={{ flexDirection: 'row', width: '100%' }}
|
|
664
|
+
>
|
|
665
|
+
{Array.from({ length: columnCount }).map((_, colIndex) =>
|
|
666
|
+
renderTableCell(
|
|
667
|
+
renderCellContent(
|
|
668
|
+
row.values?.[colIndex],
|
|
669
|
+
`${rowIndex}-${colIndex}`,
|
|
630
670
|
),
|
|
671
|
+
colIndex,
|
|
672
|
+
isLastRow,
|
|
673
|
+
colIndex,
|
|
674
|
+
),
|
|
675
|
+
)}
|
|
676
|
+
{showAddCard &&
|
|
677
|
+
renderTableCell(
|
|
678
|
+
null,
|
|
679
|
+
gridColumnCount - 1,
|
|
680
|
+
isLastRow,
|
|
681
|
+
'__add_spacer__',
|
|
631
682
|
)}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
'__add_spacer__',
|
|
638
|
-
)}
|
|
639
|
-
</View>
|
|
640
|
-
)
|
|
641
|
-
})}
|
|
642
|
-
</View>
|
|
643
|
-
</ScrollView>
|
|
644
|
-
</Accordion>
|
|
683
|
+
</View>
|
|
684
|
+
)
|
|
685
|
+
})}
|
|
686
|
+
</View>
|
|
687
|
+
</ScrollView>
|
|
645
688
|
)
|
|
646
689
|
|
|
647
690
|
const outerStyle: ViewStyle = {
|
|
@@ -680,37 +723,94 @@ function CompareTable({
|
|
|
680
723
|
}
|
|
681
724
|
|
|
682
725
|
// --- fixed-width scroll mode: chained horizontal sync + sticky cards -----
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
726
|
+
// Build children and stickyHeaderIndices for the vertical ScrollView.
|
|
727
|
+
// When stickyHeaders is on, section headers are injected as direct children
|
|
728
|
+
// so React Native's stickyHeaderIndices can pin them with stacking.
|
|
729
|
+
const scrollChildren: React.ReactNode[] = []
|
|
730
|
+
const stickyIndices: number[] = []
|
|
731
|
+
|
|
732
|
+
if (showCardsRow) {
|
|
733
|
+
stickyIndices.push(scrollChildren.length)
|
|
734
|
+
scrollChildren.push(
|
|
735
|
+
<ScrollView
|
|
736
|
+
key="cards-row"
|
|
737
|
+
ref={cardsScrollRef}
|
|
738
|
+
horizontal
|
|
739
|
+
showsHorizontalScrollIndicator={false}
|
|
740
|
+
scrollEventThrottle={16}
|
|
741
|
+
onScroll={onScroll('cards')}
|
|
742
|
+
onScrollBeginDrag={onScrollBeginDrag('cards')}
|
|
743
|
+
onScrollEndDrag={onScrollEndDrag('cards')}
|
|
744
|
+
onMomentumScrollEnd={onMomentumScrollEnd('cards')}
|
|
745
|
+
>
|
|
746
|
+
<View style={{ flexDirection: 'row', width: contentWidth }}>
|
|
747
|
+
{columns.map(renderCard)}
|
|
748
|
+
{showAddCard && renderAddCard()}
|
|
749
|
+
</View>
|
|
750
|
+
</ScrollView>,
|
|
751
|
+
)
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
sections.forEach((section, index) => {
|
|
755
|
+
const sectionKey = section.key ?? section.title ?? index
|
|
756
|
+
const headerContent = renderHeaderContent(section)
|
|
757
|
+
|
|
758
|
+
if (stickyHeaders && headerContent != null) {
|
|
759
|
+
stickyIndices.push(scrollChildren.length)
|
|
760
|
+
scrollChildren.push(
|
|
761
|
+
<View
|
|
762
|
+
key={`sticky-header-${sectionKey}`}
|
|
763
|
+
style={{ width: '100%' }}
|
|
764
|
+
>
|
|
765
|
+
{stickyHeaderLabel ? (
|
|
766
|
+
headerContent
|
|
767
|
+
) : (
|
|
768
|
+
<ScrollView
|
|
769
|
+
horizontal
|
|
770
|
+
showsHorizontalScrollIndicator={false}
|
|
771
|
+
>
|
|
772
|
+
<View style={{ width: contentWidth }}>
|
|
773
|
+
{headerContent}
|
|
774
|
+
</View>
|
|
775
|
+
</ScrollView>
|
|
776
|
+
)}
|
|
777
|
+
</View>,
|
|
778
|
+
)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
scrollChildren.push(
|
|
782
|
+
<Accordion
|
|
783
|
+
key={sectionKey}
|
|
784
|
+
title={section.title}
|
|
785
|
+
defaultExpanded={section.defaultExpanded ?? index === 0}
|
|
786
|
+
modes={modes}
|
|
787
|
+
>
|
|
788
|
+
{!stickyHeaders && headerContent != null && (
|
|
789
|
+
stickyHeaderLabel ? headerContent : (
|
|
790
|
+
<ScrollView
|
|
791
|
+
horizontal
|
|
792
|
+
showsHorizontalScrollIndicator={false}
|
|
793
|
+
>
|
|
794
|
+
<View style={{ width: contentWidth }}>
|
|
795
|
+
{headerContent}
|
|
796
|
+
</View>
|
|
797
|
+
</ScrollView>
|
|
798
|
+
)
|
|
799
|
+
)}
|
|
800
|
+
{renderBodyContent(section, index)}
|
|
801
|
+
</Accordion>,
|
|
802
|
+
)
|
|
803
|
+
})
|
|
804
|
+
|
|
688
805
|
return (
|
|
689
806
|
<View style={[outerStyle, style, { flexDirection: 'column', flex: 1 }]}>
|
|
690
807
|
<ScrollView
|
|
691
808
|
style={{ width: '100%', flex: 1 }}
|
|
692
|
-
stickyHeaderIndices={
|
|
809
|
+
stickyHeaderIndices={stickyIndices}
|
|
693
810
|
showsVerticalScrollIndicator={false}
|
|
694
811
|
directionalLockEnabled
|
|
695
812
|
>
|
|
696
|
-
{
|
|
697
|
-
<ScrollView
|
|
698
|
-
ref={cardsScrollRef}
|
|
699
|
-
horizontal
|
|
700
|
-
showsHorizontalScrollIndicator={false}
|
|
701
|
-
scrollEventThrottle={16}
|
|
702
|
-
onScroll={onCardsScroll}
|
|
703
|
-
>
|
|
704
|
-
<View style={{ flexDirection: 'row', width: contentWidth }}>
|
|
705
|
-
{columns.map(renderCard)}
|
|
706
|
-
{showAddCard && renderAddCard()}
|
|
707
|
-
</View>
|
|
708
|
-
</ScrollView>
|
|
709
|
-
)}
|
|
710
|
-
|
|
711
|
-
<View style={{ width: '100%' }}>
|
|
712
|
-
{sections.map(renderSection)}
|
|
713
|
-
</View>
|
|
813
|
+
{scrollChildren}
|
|
714
814
|
</ScrollView>
|
|
715
815
|
</View>
|
|
716
816
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useContext, useEffect, useMemo } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Platform,
|
|
4
|
+
ScrollView,
|
|
4
5
|
Text,
|
|
5
6
|
View,
|
|
6
7
|
useWindowDimensions,
|
|
@@ -100,6 +101,12 @@ export type ContentSheetProps = {
|
|
|
100
101
|
* `useAnimatedStyle`. Default `true`.
|
|
101
102
|
*/
|
|
102
103
|
visible?: boolean
|
|
104
|
+
/**
|
|
105
|
+
* Max height of the sheet as a fraction (0–1) of the screen height.
|
|
106
|
+
* When content reaches this height the sheet becomes vertically scrollable.
|
|
107
|
+
* Default `0.7`.
|
|
108
|
+
*/
|
|
109
|
+
maxHeightPercent?: number
|
|
103
110
|
/** Optional style override applied to the sheet container. */
|
|
104
111
|
style?: StyleProp<ViewStyle>
|
|
105
112
|
} & Omit<ViewProps, 'style' | 'children'>
|
|
@@ -202,6 +209,7 @@ function ContentSheet({
|
|
|
202
209
|
safeAreaBottom = true,
|
|
203
210
|
pinToBottom = true,
|
|
204
211
|
visible = true,
|
|
212
|
+
maxHeightPercent = 0.7,
|
|
205
213
|
style,
|
|
206
214
|
...rest
|
|
207
215
|
}: ContentSheetProps) {
|
|
@@ -220,22 +228,24 @@ function ContentSheet({
|
|
|
220
228
|
const paddingBottom =
|
|
221
229
|
resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0)
|
|
222
230
|
|
|
231
|
+
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
232
|
+
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
233
|
+
// height parks it fully below the bottom edge. Springing it back to `0`
|
|
234
|
+
// produces the same bottom pop-up the Drawer uses. The shared value is
|
|
235
|
+
// initialised off-screen so the very first mount pops in (no flash).
|
|
236
|
+
const { height: windowHeight } = useWindowDimensions()
|
|
237
|
+
const maxHeight = windowHeight * maxHeightPercent
|
|
238
|
+
|
|
223
239
|
const containerStyle = useMemo<StyleProp<ViewStyle>>(
|
|
224
240
|
() => [
|
|
225
241
|
pinToBottom ? pinnedStyle : null,
|
|
226
242
|
resolved.container,
|
|
227
|
-
{ paddingBottom },
|
|
243
|
+
{ maxHeight, paddingBottom },
|
|
228
244
|
style,
|
|
229
245
|
],
|
|
230
|
-
[pinToBottom, resolved.container, paddingBottom, style]
|
|
246
|
+
[pinToBottom, resolved.container, maxHeight, paddingBottom, style]
|
|
231
247
|
)
|
|
232
248
|
|
|
233
|
-
// Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
|
|
234
|
-
// sheet is bottom-anchored, so a positive `translateY` equal to the window
|
|
235
|
-
// height parks it fully below the bottom edge. Springing it back to `0`
|
|
236
|
-
// produces the same bottom pop-up the Drawer uses. The shared value is
|
|
237
|
-
// initialised off-screen so the very first mount pops in (no flash).
|
|
238
|
-
const { height: windowHeight } = useWindowDimensions()
|
|
239
249
|
const entrance = useSharedValue(windowHeight)
|
|
240
250
|
useEffect(() => {
|
|
241
251
|
entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG)
|
|
@@ -259,6 +269,7 @@ function ContentSheet({
|
|
|
259
269
|
style={containerStyle}
|
|
260
270
|
spacing={keyboardSpacing}
|
|
261
271
|
entrance={entrance}
|
|
272
|
+
maxHeight={maxHeight}
|
|
262
273
|
{...rest}
|
|
263
274
|
>
|
|
264
275
|
{header}
|
|
@@ -268,7 +279,7 @@ function ContentSheet({
|
|
|
268
279
|
}
|
|
269
280
|
|
|
270
281
|
return (
|
|
271
|
-
<AnimatedSheet style={containerStyle} entrance={entrance} {...rest}>
|
|
282
|
+
<AnimatedSheet style={containerStyle} entrance={entrance} maxHeight={maxHeight} {...rest}>
|
|
272
283
|
{header}
|
|
273
284
|
{processedChildren}
|
|
274
285
|
</AnimatedSheet>
|
|
@@ -279,12 +290,14 @@ type KeyboardAwareSheetProps = {
|
|
|
279
290
|
style: StyleProp<ViewStyle>
|
|
280
291
|
spacing: number
|
|
281
292
|
entrance: SharedValue<number>
|
|
293
|
+
maxHeight: number
|
|
282
294
|
children: React.ReactNode
|
|
283
295
|
} & Omit<ViewProps, 'style' | 'children'>
|
|
284
296
|
|
|
285
297
|
type AnimatedSheetProps = {
|
|
286
298
|
style: StyleProp<ViewStyle>
|
|
287
299
|
entrance: SharedValue<number>
|
|
300
|
+
maxHeight: number
|
|
288
301
|
children: React.ReactNode
|
|
289
302
|
} & Omit<ViewProps, 'style' | 'children'>
|
|
290
303
|
|
|
@@ -300,6 +313,7 @@ function KeyboardAwareSheet({
|
|
|
300
313
|
style,
|
|
301
314
|
spacing,
|
|
302
315
|
entrance,
|
|
316
|
+
maxHeight,
|
|
303
317
|
children,
|
|
304
318
|
...rest
|
|
305
319
|
}: KeyboardAwareSheetProps) {
|
|
@@ -315,7 +329,14 @@ function KeyboardAwareSheet({
|
|
|
315
329
|
|
|
316
330
|
return (
|
|
317
331
|
<Animated.View style={[style, animatedStyle]} {...rest}>
|
|
318
|
-
|
|
332
|
+
<ScrollView
|
|
333
|
+
style={{ maxHeight }}
|
|
334
|
+
bounces={false}
|
|
335
|
+
showsVerticalScrollIndicator={false}
|
|
336
|
+
keyboardShouldPersistTaps="handled"
|
|
337
|
+
>
|
|
338
|
+
{children}
|
|
339
|
+
</ScrollView>
|
|
319
340
|
</Animated.View>
|
|
320
341
|
)
|
|
321
342
|
}
|
|
@@ -328,6 +349,7 @@ function KeyboardAwareSheet({
|
|
|
328
349
|
function AnimatedSheet({
|
|
329
350
|
style,
|
|
330
351
|
entrance,
|
|
352
|
+
maxHeight,
|
|
331
353
|
children,
|
|
332
354
|
...rest
|
|
333
355
|
}: AnimatedSheetProps) {
|
|
@@ -339,7 +361,14 @@ function AnimatedSheet({
|
|
|
339
361
|
|
|
340
362
|
return (
|
|
341
363
|
<Animated.View style={[style, animatedStyle]} {...rest}>
|
|
342
|
-
|
|
364
|
+
<ScrollView
|
|
365
|
+
style={{ maxHeight }}
|
|
366
|
+
bounces={false}
|
|
367
|
+
showsVerticalScrollIndicator={false}
|
|
368
|
+
keyboardShouldPersistTaps="handled"
|
|
369
|
+
>
|
|
370
|
+
{children}
|
|
371
|
+
</ScrollView>
|
|
343
372
|
</Animated.View>
|
|
344
373
|
)
|
|
345
374
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
type ViewStyle,
|
|
6
|
+
type TextStyle,
|
|
7
|
+
} from 'react-native'
|
|
8
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
9
|
+
import { EMPTY_MODES } from '../../utils/react-utils'
|
|
10
|
+
import type { Modes } from '../../design-tokens'
|
|
11
|
+
|
|
12
|
+
export type CounterBadgeProps = {
|
|
13
|
+
/** The value shown inside the badge. */
|
|
14
|
+
value?: string
|
|
15
|
+
/** Modes used to resolve design tokens. Counter Badge exposes a single `Default` mode. */
|
|
16
|
+
modes?: Modes
|
|
17
|
+
style?: ViewStyle
|
|
18
|
+
} & Omit<React.ComponentProps<typeof View>, 'style'>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Counter Badge — a small token-driven pill that displays a value.
|
|
22
|
+
*
|
|
23
|
+
* All visual attributes resolve from the Figma `counterBadge/*` tokens via
|
|
24
|
+
* `getVariableByName`.
|
|
25
|
+
*/
|
|
26
|
+
function CounterBadge({
|
|
27
|
+
value = '99',
|
|
28
|
+
modes = EMPTY_MODES,
|
|
29
|
+
style,
|
|
30
|
+
...rest
|
|
31
|
+
}: CounterBadgeProps) {
|
|
32
|
+
// Resolve token values (fall back to the Figma defaults for the single
|
|
33
|
+
// `Default` mode so the badge still renders if a token is missing).
|
|
34
|
+
const backgroundColor =
|
|
35
|
+
(getVariableByName('counterBadge/background', modes) as string) ?? '#ebebec'
|
|
36
|
+
const foreground =
|
|
37
|
+
(getVariableByName('counterBadge/foreground', modes) as string) ?? '#0c0d10'
|
|
38
|
+
const fontFamily =
|
|
39
|
+
(getVariableByName('counterBadge/fontFamily', modes) as string) ?? 'JioType Var'
|
|
40
|
+
const fontWeightRaw =
|
|
41
|
+
getVariableByName('counterBadge/fontWeight', modes) ?? 500
|
|
42
|
+
const fontWeight =
|
|
43
|
+
typeof fontWeightRaw === 'number' ? String(fontWeightRaw) : (fontWeightRaw as string)
|
|
44
|
+
const fontSize =
|
|
45
|
+
Number(getVariableByName('counterBadge/fontSize', modes) ?? 8)
|
|
46
|
+
const lineHeight =
|
|
47
|
+
Number(getVariableByName('counterBadge/lineHeight', modes) ?? 8)
|
|
48
|
+
const paddingHorizontal =
|
|
49
|
+
Number(getVariableByName('counterBadge/padding/horizontal', modes) ?? 4)
|
|
50
|
+
// The Figma design pins vertical padding to 4px (matching horizontal), but the
|
|
51
|
+
// committed variables snapshot resolves `counterBadge/padding/vertical` to 5px
|
|
52
|
+
// (a stale export). Figma is the source of truth here, so hardcode 4px rather
|
|
53
|
+
// than trust the token, keeping the badge one pixel shorter to match the design.
|
|
54
|
+
const paddingVertical = 4
|
|
55
|
+
const borderRadius =
|
|
56
|
+
Number(getVariableByName('counterBadge/radius', modes) ?? 999)
|
|
57
|
+
|
|
58
|
+
const containerStyle: ViewStyle = {
|
|
59
|
+
backgroundColor,
|
|
60
|
+
paddingHorizontal,
|
|
61
|
+
paddingVertical,
|
|
62
|
+
borderRadius,
|
|
63
|
+
alignSelf: 'flex-start',
|
|
64
|
+
alignItems: 'center',
|
|
65
|
+
justifyContent: 'center',
|
|
66
|
+
...((style as any) || {}),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const textStyle: TextStyle = {
|
|
70
|
+
color: foreground,
|
|
71
|
+
fontFamily,
|
|
72
|
+
fontWeight: fontWeight as TextStyle['fontWeight'],
|
|
73
|
+
fontSize,
|
|
74
|
+
lineHeight,
|
|
75
|
+
textAlign: 'center',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View style={containerStyle} {...rest}>
|
|
80
|
+
{/* Figma `wrap`: a centered box with a 10px minimum. */}
|
|
81
|
+
<View style={WRAP_STYLE}>
|
|
82
|
+
<Text style={textStyle}>{value}</Text>
|
|
83
|
+
</View>
|
|
84
|
+
</View>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Figma `wrap` node (data-node-id 7701:10879): min-width 10, centered.
|
|
89
|
+
const WRAP_STYLE: ViewStyle = {
|
|
90
|
+
minWidth: 10,
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
justifyContent: 'center',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default CounterBadge
|