@retray-dev/ui-kit 6.2.0 → 9.0.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/COMPONENTS.md +997 -20
- package/EXAMPLES.md +250 -2
- package/README.md +21 -14
- package/dist/Accordion.d.mts +28 -0
- package/dist/Accordion.d.ts +28 -0
- package/dist/Accordion.js +392 -0
- package/dist/Accordion.mjs +7 -0
- package/dist/AlertBanner.d.mts +16 -0
- package/dist/AlertBanner.d.ts +16 -0
- package/dist/AlertBanner.js +250 -0
- package/dist/AlertBanner.mjs +6 -0
- package/dist/AppHeader.d.mts +40 -0
- package/dist/AppHeader.d.ts +40 -0
- package/dist/AppHeader.js +515 -0
- package/dist/AppHeader.mjs +10 -0
- package/dist/Avatar.d.mts +20 -0
- package/dist/Avatar.d.ts +20 -0
- package/dist/Avatar.js +244 -0
- package/dist/Avatar.mjs +4 -0
- package/dist/Badge.d.mts +26 -0
- package/dist/Badge.d.ts +26 -0
- package/dist/Badge.js +257 -0
- package/dist/Badge.mjs +5 -0
- package/dist/Button.d.mts +30 -0
- package/dist/Button.d.ts +30 -0
- package/dist/Button.js +432 -0
- package/dist/Button.mjs +9 -0
- package/dist/ButtonGroup.d.mts +26 -0
- package/dist/ButtonGroup.d.ts +26 -0
- package/dist/ButtonGroup.js +52 -0
- package/dist/ButtonGroup.mjs +3 -0
- package/dist/Card.d.mts +39 -0
- package/dist/Card.d.ts +39 -0
- package/dist/Card.js +349 -0
- package/dist/Card.mjs +8 -0
- package/dist/CategoryStrip.d.mts +26 -0
- package/dist/CategoryStrip.d.ts +26 -0
- package/dist/CategoryStrip.js +453 -0
- package/dist/CategoryStrip.mjs +9 -0
- package/dist/Checkbox.d.mts +14 -0
- package/dist/Checkbox.d.ts +14 -0
- package/dist/Checkbox.js +336 -0
- package/dist/Checkbox.mjs +7 -0
- package/dist/Chip.d.mts +31 -0
- package/dist/Chip.d.ts +31 -0
- package/dist/Chip.js +403 -0
- package/dist/Chip.mjs +8 -0
- package/dist/ConfirmDialog.d.mts +15 -0
- package/dist/ConfirmDialog.d.ts +15 -0
- package/dist/ConfirmDialog.js +560 -0
- package/dist/ConfirmDialog.mjs +10 -0
- package/dist/CurrencyDisplay.d.mts +24 -0
- package/dist/CurrencyDisplay.d.ts +24 -0
- package/dist/CurrencyDisplay.js +189 -0
- package/dist/CurrencyDisplay.mjs +4 -0
- package/dist/CurrencyInput.d.mts +26 -0
- package/dist/CurrencyInput.d.ts +26 -0
- package/dist/CurrencyInput.js +408 -0
- package/dist/CurrencyInput.mjs +8 -0
- package/dist/DetailRow.d.mts +32 -0
- package/dist/DetailRow.d.ts +32 -0
- package/dist/DetailRow.js +275 -0
- package/dist/DetailRow.mjs +5 -0
- package/dist/EmptyState.d.mts +27 -0
- package/dist/EmptyState.d.ts +27 -0
- package/dist/EmptyState.js +523 -0
- package/dist/EmptyState.mjs +10 -0
- package/dist/ErrorBoundary.d.mts +42 -0
- package/dist/ErrorBoundary.d.ts +42 -0
- package/dist/ErrorBoundary.js +351 -0
- package/dist/ErrorBoundary.mjs +7 -0
- package/dist/Form.d.mts +52 -0
- package/dist/Form.d.ts +52 -0
- package/dist/Form.js +204 -0
- package/dist/Form.mjs +4 -0
- package/dist/HolographicCard.d.mts +55 -0
- package/dist/HolographicCard.d.ts +55 -0
- package/dist/HolographicCard.js +316 -0
- package/dist/HolographicCard.mjs +191 -0
- package/dist/IconButton.d.mts +27 -0
- package/dist/IconButton.d.ts +27 -0
- package/dist/IconButton.js +400 -0
- package/dist/IconButton.mjs +8 -0
- package/dist/ImageViewer.d.mts +23 -0
- package/dist/ImageViewer.d.ts +23 -0
- package/dist/ImageViewer.js +582 -0
- package/dist/ImageViewer.mjs +8 -0
- package/dist/Input.d.mts +23 -0
- package/dist/Input.d.ts +23 -0
- package/dist/Input.js +351 -0
- package/dist/Input.mjs +7 -0
- package/dist/LabelValue.d.mts +16 -0
- package/dist/LabelValue.d.ts +16 -0
- package/dist/LabelValue.js +225 -0
- package/dist/LabelValue.mjs +5 -0
- package/dist/ListGroup.d.mts +34 -0
- package/dist/ListGroup.d.ts +34 -0
- package/dist/ListGroup.js +217 -0
- package/dist/ListGroup.mjs +5 -0
- package/dist/ListItem.d.mts +64 -0
- package/dist/ListItem.d.ts +64 -0
- package/dist/ListItem.js +444 -0
- package/dist/ListItem.mjs +9 -0
- package/dist/MediaCard.d.mts +39 -0
- package/dist/MediaCard.d.ts +39 -0
- package/dist/MediaCard.js +475 -0
- package/dist/MediaCard.mjs +9 -0
- package/dist/MenuGroup.d.mts +34 -0
- package/dist/MenuGroup.d.ts +34 -0
- package/dist/MenuGroup.js +217 -0
- package/dist/MenuGroup.mjs +5 -0
- package/dist/MenuItem.d.mts +48 -0
- package/dist/MenuItem.d.ts +48 -0
- package/dist/MenuItem.js +415 -0
- package/dist/MenuItem.mjs +9 -0
- package/dist/MonthPicker.d.mts +28 -0
- package/dist/MonthPicker.d.ts +28 -0
- package/dist/MonthPicker.js +297 -0
- package/dist/MonthPicker.mjs +5 -0
- package/dist/PagerDots.d.mts +35 -0
- package/dist/PagerDots.d.ts +35 -0
- package/dist/PagerDots.js +392 -0
- package/dist/PagerDots.mjs +7 -0
- package/dist/Pressable.d.mts +34 -0
- package/dist/Pressable.d.ts +34 -0
- package/dist/Pressable.js +143 -0
- package/dist/Pressable.mjs +5 -0
- package/dist/PricingCard.d.mts +50 -0
- package/dist/PricingCard.d.ts +50 -0
- package/dist/PricingCard.js +636 -0
- package/dist/PricingCard.mjs +11 -0
- package/dist/Progress.d.mts +14 -0
- package/dist/Progress.d.ts +14 -0
- package/dist/Progress.js +191 -0
- package/dist/Progress.mjs +5 -0
- package/dist/RadioGroup.d.mts +19 -0
- package/dist/RadioGroup.d.ts +19 -0
- package/dist/RadioGroup.js +392 -0
- package/dist/RadioGroup.mjs +7 -0
- package/dist/RetrayProvider.d.mts +2 -0
- package/dist/RetrayProvider.d.ts +2 -0
- package/dist/RetrayProvider.js +214 -0
- package/dist/RetrayProvider.mjs +5 -0
- package/dist/Select.d.mts +22 -0
- package/dist/Select.d.ts +22 -0
- package/dist/Select.js +488 -0
- package/dist/Select.mjs +7 -0
- package/dist/SelectableGrid.d.mts +44 -0
- package/dist/SelectableGrid.d.ts +44 -0
- package/dist/SelectableGrid.js +448 -0
- package/dist/SelectableGrid.mjs +9 -0
- package/dist/Separator.d.mts +10 -0
- package/dist/Separator.d.ts +10 -0
- package/dist/Separator.js +156 -0
- package/dist/Separator.mjs +3 -0
- package/dist/Sheet.d.mts +93 -0
- package/dist/Sheet.d.ts +93 -0
- package/dist/Sheet.js +450 -0
- package/dist/Sheet.mjs +6 -0
- package/dist/Skeleton.d.mts +67 -0
- package/dist/Skeleton.d.ts +67 -0
- package/dist/Skeleton.js +266 -0
- package/dist/Skeleton.mjs +6 -0
- package/dist/Slider.d.mts +20 -0
- package/dist/Slider.d.ts +20 -0
- package/dist/Slider.js +279 -0
- package/dist/Slider.mjs +5 -0
- package/dist/Spinner.d.mts +12 -0
- package/dist/Spinner.d.ts +12 -0
- package/dist/Spinner.js +193 -0
- package/dist/Spinner.mjs +4 -0
- package/dist/Switch.d.mts +13 -0
- package/dist/Switch.d.ts +13 -0
- package/dist/Switch.js +311 -0
- package/dist/Switch.mjs +6 -0
- package/dist/TabBar.d.mts +42 -0
- package/dist/TabBar.d.ts +42 -0
- package/dist/TabBar.js +361 -0
- package/dist/TabBar.mjs +6 -0
- package/dist/Tabs.d.mts +27 -0
- package/dist/Tabs.d.ts +27 -0
- package/dist/Tabs.js +419 -0
- package/dist/Tabs.mjs +7 -0
- package/dist/Text.d.mts +12 -0
- package/dist/Text.d.ts +12 -0
- package/dist/Text.js +327 -0
- package/dist/Text.mjs +5 -0
- package/dist/Textarea.d.mts +16 -0
- package/dist/Textarea.d.ts +16 -0
- package/dist/Textarea.js +333 -0
- package/dist/Textarea.mjs +7 -0
- package/dist/Toast.d.mts +47 -0
- package/dist/Toast.d.ts +47 -0
- package/dist/Toast.js +185 -0
- package/dist/Toast.mjs +4 -0
- package/dist/Toggle.d.mts +36 -0
- package/dist/Toggle.d.ts +36 -0
- package/dist/Toggle.js +412 -0
- package/dist/Toggle.mjs +8 -0
- package/dist/VirtualList.d.mts +19 -0
- package/dist/VirtualList.d.ts +19 -0
- package/dist/VirtualList.js +38 -0
- package/dist/VirtualList.mjs +2 -0
- package/dist/chunk-26BCI223.mjs +14 -0
- package/dist/chunk-2CE3TQVY.mjs +11 -0
- package/dist/chunk-2TFTAWVJ.mjs +131 -0
- package/dist/chunk-2UYENBLV.mjs +49 -0
- package/dist/chunk-3BBOZ3OQ.mjs +41 -0
- package/dist/chunk-3DKJ2GIC.mjs +30 -0
- package/dist/chunk-3U4SSNWP.mjs +120 -0
- package/dist/chunk-4I7D47FH.mjs +139 -0
- package/dist/chunk-4K625MVM.mjs +142 -0
- package/dist/chunk-6OAZJ577.mjs +98 -0
- package/dist/chunk-6Q64UFIA.mjs +71 -0
- package/dist/chunk-756RAKE4.mjs +145 -0
- package/dist/chunk-7QHVVCB3.mjs +115 -0
- package/dist/chunk-A3A6KNQN.mjs +245 -0
- package/dist/chunk-A4MDAP7G.mjs +42 -0
- package/dist/chunk-AJ7ZDNBT.mjs +120 -0
- package/dist/chunk-AV4EMIRH.mjs +94 -0
- package/dist/chunk-AZJF2BLK.mjs +115 -0
- package/dist/chunk-BNP626TY.mjs +159 -0
- package/dist/chunk-BRKYVJVV.mjs +60 -0
- package/dist/chunk-DVK4G2GT.mjs +59 -0
- package/dist/chunk-EH745HE5.mjs +127 -0
- package/dist/chunk-EJ7ZPXOH.mjs +163 -0
- package/dist/chunk-GD6KXMG5.mjs +106 -0
- package/dist/chunk-GQYFLP3D.mjs +187 -0
- package/dist/chunk-ID72TK46.mjs +111 -0
- package/dist/chunk-IRRY3CRZ.mjs +82 -0
- package/dist/chunk-JB67UOB5.mjs +92 -0
- package/dist/chunk-JMOZEC77.mjs +90 -0
- package/dist/chunk-JT7HKXRB.mjs +114 -0
- package/dist/chunk-KIHCWCWL.mjs +124 -0
- package/dist/chunk-LXJIIOYQ.mjs +104 -0
- package/dist/chunk-M6ZXVBTK.mjs +64 -0
- package/dist/chunk-MAC465BB.mjs +61 -0
- package/dist/chunk-MBMXYJJV.mjs +36 -0
- package/dist/chunk-MLF3EZFW.mjs +119 -0
- package/dist/chunk-MX6HRKMI.mjs +29 -0
- package/dist/chunk-NA7PARID.mjs +147 -0
- package/dist/chunk-NC5ZTR2Y.mjs +32 -0
- package/dist/chunk-O3HA6TYM.mjs +139 -0
- package/dist/chunk-OB4JUQ3O.mjs +51 -0
- package/dist/chunk-PFZTM6D5.mjs +238 -0
- package/dist/chunk-QKH5ZOD5.mjs +97 -0
- package/dist/chunk-QY3X2UYR.mjs +191 -0
- package/dist/chunk-SOA2Z4RB.mjs +82 -0
- package/dist/chunk-SOYNZDVY.mjs +151 -0
- package/dist/chunk-T7XZ7H7Y.mjs +57 -0
- package/dist/chunk-TERDKCLE.mjs +74 -0
- package/dist/chunk-UREA2GYY.mjs +113 -0
- package/dist/chunk-VGTDN7SW.mjs +164 -0
- package/dist/chunk-VQ57HWPL.mjs +144 -0
- package/dist/chunk-WBOOUHSS.mjs +62 -0
- package/dist/chunk-WJLKJMKR.mjs +78 -0
- package/dist/chunk-X4G6APW6.mjs +134 -0
- package/dist/chunk-Y6FXYEAI.mjs +8 -0
- package/dist/chunk-YFZ3ELX5.mjs +16 -0
- package/dist/chunk-YNROWHQJ.mjs +46 -0
- package/dist/chunk-Z4BVUWW6.mjs +196 -0
- package/dist/chunk-ZJKGQMYH.mjs +131 -0
- package/dist/index-wt-orHUi.d.mts +85 -0
- package/dist/index-wt-orHUi.d.ts +85 -0
- package/dist/index.d.mts +149 -920
- package/dist/index.d.ts +149 -920
- package/dist/index.js +2560 -970
- package/dist/index.mjs +60 -3895
- package/package.json +55 -16
- package/src/assets/fonts/Sohne-Bold.otf +0 -0
- package/src/assets/fonts/Sohne-BoldItalic.otf +0 -0
- package/src/assets/fonts/Sohne-ExtraBold.otf +0 -0
- package/src/assets/fonts/Sohne-ExtraBoldItalic.otf +0 -0
- package/src/assets/fonts/Sohne-ExtraLight.otf +0 -0
- package/src/assets/fonts/Sohne-ExtraLightItalic.otf +0 -0
- package/src/assets/fonts/Sohne-Italic.otf +0 -0
- package/src/assets/fonts/Sohne-Light.otf +0 -0
- package/src/assets/fonts/Sohne-LightItalic.otf +0 -0
- package/src/assets/fonts/Sohne-Medium.otf +0 -0
- package/src/assets/fonts/Sohne-MediumItalic.otf +0 -0
- package/src/assets/fonts/Sohne-Regular.otf +0 -0
- package/src/assets/fonts/Sohne-SemiBold.otf +0 -0
- package/src/assets/fonts/Sohne-SemiBoldItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-Bold.otf +0 -0
- package/src/assets/fonts/SohneMono-BoldItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-ExtraBold.otf +0 -0
- package/src/assets/fonts/SohneMono-ExtraBoldItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-ExtraLight.otf +0 -0
- package/src/assets/fonts/SohneMono-ExtraLightItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-Italic.otf +0 -0
- package/src/assets/fonts/SohneMono-Light.otf +0 -0
- package/src/assets/fonts/SohneMono-LightItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-Medium.otf +0 -0
- package/src/assets/fonts/SohneMono-MediumItalic.otf +0 -0
- package/src/assets/fonts/SohneMono-Regular.otf +0 -0
- package/src/assets/fonts/SohneMono-SemiBold.otf +0 -0
- package/src/assets/fonts/SohneMono-SemiBoldItalic.otf +0 -0
- package/src/components/Accordion/Accordion.tsx +15 -4
- package/src/components/AlertBanner/AlertBanner.tsx +38 -12
- package/src/components/AppHeader/AppHeader.tsx +172 -0
- package/src/components/AppHeader/index.ts +1 -0
- package/src/components/Avatar/Avatar.tsx +14 -4
- package/src/components/Badge/Badge.tsx +12 -3
- package/src/components/Button/Button.tsx +30 -38
- package/src/components/ButtonGroup/ButtonGroup.tsx +13 -10
- package/src/components/Card/Card.tsx +29 -57
- package/src/components/CategoryStrip/CategoryStrip.tsx +41 -42
- package/src/components/Checkbox/Checkbox.tsx +36 -45
- package/src/components/Chip/Chip.tsx +41 -48
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +2 -2
- package/src/components/CurrencyDisplay/CurrencyDisplay.tsx +4 -2
- package/src/components/CurrencyInput/CurrencyInput.tsx +12 -10
- package/src/components/DetailRow/DetailRow.tsx +9 -7
- package/src/components/EmptyState/EmptyState.tsx +4 -3
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +153 -0
- package/src/components/ErrorBoundary/index.ts +1 -0
- package/src/components/Form/Form.tsx +149 -0
- package/src/components/Form/index.ts +1 -0
- package/src/components/HolographicCard/HolographicCard.tsx +315 -0
- package/src/components/HolographicCard/index.ts +1 -0
- package/src/components/IconButton/IconButton.tsx +23 -29
- package/src/components/ImageViewer/ImageViewer.tsx +290 -0
- package/src/components/ImageViewer/index.ts +1 -0
- package/src/components/Input/Input.tsx +27 -31
- package/src/components/LabelValue/LabelValue.tsx +6 -4
- package/src/components/ListGroup/ListGroup.tsx +145 -0
- package/src/components/ListGroup/index.ts +1 -0
- package/src/components/ListItem/ListItem.tsx +78 -76
- package/src/components/MediaCard/MediaCard.tsx +15 -7
- package/src/components/MenuGroup/MenuGroup.tsx +145 -0
- package/src/components/MenuGroup/index.ts +1 -0
- package/src/components/MenuItem/MenuItem.tsx +16 -33
- package/src/components/MonthPicker/MonthPicker.tsx +41 -15
- package/src/components/MonthPicker/index.ts +1 -1
- package/src/components/PagerDots/PagerDots.tsx +200 -0
- package/src/components/PagerDots/index.ts +1 -0
- package/src/components/Pressable/Pressable.tsx +19 -35
- package/src/components/PricingCard/PricingCard.tsx +220 -0
- package/src/components/PricingCard/index.ts +1 -0
- package/src/components/RadioGroup/RadioGroup.tsx +23 -39
- package/src/components/RetrayProvider/RetrayProvider.tsx +59 -0
- package/src/components/RetrayProvider/index.ts +1 -0
- package/src/components/Select/Select.tsx +6 -6
- package/src/components/SelectableGrid/SelectableGrid.tsx +205 -0
- package/src/components/SelectableGrid/index.ts +1 -0
- package/src/components/Separator/Separator.tsx +1 -3
- package/src/components/Sheet/Sheet.tsx +146 -18
- package/src/components/Skeleton/Skeleton.tsx +143 -2
- package/src/components/Slider/Slider.tsx +2 -2
- package/src/components/Spinner/Spinner.tsx +18 -3
- package/src/components/Switch/Switch.tsx +44 -49
- package/src/components/TabBar/TabBar.tsx +169 -0
- package/src/components/TabBar/index.ts +1 -0
- package/src/components/Tabs/Tabs.tsx +45 -44
- package/src/components/Text/Text.tsx +5 -1
- package/src/components/Textarea/Textarea.tsx +18 -14
- package/src/components/Toast/Toast.tsx +6 -6
- package/src/components/Toggle/Toggle.tsx +80 -72
- package/src/components/VirtualList/VirtualList.tsx +60 -0
- package/src/components/VirtualList/index.ts +1 -0
- package/src/fonts.ts +41 -20
- package/src/index.ts +28 -3
- package/src/theme/colors.ts +53 -39
- package/src/theme/types.ts +3 -0
- package/src/tokens.ts +49 -39
- package/src/utils/animations.ts +29 -1
- package/src/utils/fontGuard.ts +34 -0
- package/src/utils/haptics.ts +211 -9
- package/src/utils/icons.ts +47 -20
- package/src/utils/pressable.ts +66 -0
- package/src/utils/usePressScale.ts +2 -0
- package/src/assets/fonts/Poppins-Black.ttf +0 -0
- package/src/assets/fonts/Poppins-BlackItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-Bold.ttf +0 -0
- package/src/assets/fonts/Poppins-BoldItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-ExtraBold.ttf +0 -0
- package/src/assets/fonts/Poppins-ExtraBoldItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-ExtraLight.ttf +0 -0
- package/src/assets/fonts/Poppins-ExtraLightItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-Italic.ttf +0 -0
- package/src/assets/fonts/Poppins-Light.ttf +0 -0
- package/src/assets/fonts/Poppins-LightItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-Medium.ttf +0 -0
- package/src/assets/fonts/Poppins-MediumItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-Regular.ttf +0 -0
- package/src/assets/fonts/Poppins-SemiBold.ttf +0 -0
- package/src/assets/fonts/Poppins-SemiBoldItalic.ttf +0 -0
- package/src/assets/fonts/Poppins-Thin.ttf +0 -0
- package/src/assets/fonts/Poppins-ThinItalic.ttf +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Modal,
|
|
4
|
+
View,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
useWindowDimensions,
|
|
8
|
+
ImageSourcePropType,
|
|
9
|
+
ScrollView,
|
|
10
|
+
NativeSyntheticEvent,
|
|
11
|
+
NativeScrollEvent,
|
|
12
|
+
} from 'react-native'
|
|
13
|
+
import { GestureHandlerRootView, GestureDetector, Gesture } from 'react-native-gesture-handler'
|
|
14
|
+
import Animated, {
|
|
15
|
+
useSharedValue,
|
|
16
|
+
useAnimatedStyle,
|
|
17
|
+
withTiming,
|
|
18
|
+
runOnJS,
|
|
19
|
+
} from 'react-native-reanimated'
|
|
20
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
21
|
+
import { renderIcon } from '../../utils/icons'
|
|
22
|
+
import { PagerDots } from '../PagerDots'
|
|
23
|
+
import { s, vs } from '../../utils/scaling'
|
|
24
|
+
|
|
25
|
+
const MAX_SCALE = 3
|
|
26
|
+
const DOUBLE_TAP_SCALE = 2.5
|
|
27
|
+
|
|
28
|
+
interface ZoomableImageProps {
|
|
29
|
+
source: ImageSourcePropType
|
|
30
|
+
width: number
|
|
31
|
+
height: number
|
|
32
|
+
/** Reports whether this page is currently zoomed in, so the pager can lock paging. */
|
|
33
|
+
onZoomChange: (zoomed: boolean) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ZoomableImage({ source, width, height, onZoomChange }: ZoomableImageProps) {
|
|
37
|
+
const scale = useSharedValue(1)
|
|
38
|
+
const savedScale = useSharedValue(1)
|
|
39
|
+
const translateX = useSharedValue(0)
|
|
40
|
+
const translateY = useSharedValue(0)
|
|
41
|
+
const savedX = useSharedValue(0)
|
|
42
|
+
const savedY = useSharedValue(0)
|
|
43
|
+
|
|
44
|
+
const reportZoom = useCallback((zoomed: boolean) => onZoomChange(zoomed), [onZoomChange])
|
|
45
|
+
|
|
46
|
+
const reset = () => {
|
|
47
|
+
'worklet'
|
|
48
|
+
scale.value = withTiming(1)
|
|
49
|
+
savedScale.value = 1
|
|
50
|
+
translateX.value = withTiming(0)
|
|
51
|
+
translateY.value = withTiming(0)
|
|
52
|
+
savedX.value = 0
|
|
53
|
+
savedY.value = 0
|
|
54
|
+
runOnJS(reportZoom)(false)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const pinch = Gesture.Pinch()
|
|
58
|
+
.onUpdate((e) => {
|
|
59
|
+
scale.value = Math.max(1, Math.min(savedScale.value * e.scale, MAX_SCALE))
|
|
60
|
+
})
|
|
61
|
+
.onEnd(() => {
|
|
62
|
+
savedScale.value = scale.value
|
|
63
|
+
if (scale.value <= 1) {
|
|
64
|
+
reset()
|
|
65
|
+
} else {
|
|
66
|
+
runOnJS(reportZoom)(true)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const pan = Gesture.Pan()
|
|
71
|
+
.onUpdate((e) => {
|
|
72
|
+
if (scale.value <= 1) return
|
|
73
|
+
translateX.value = savedX.value + e.translationX
|
|
74
|
+
translateY.value = savedY.value + e.translationY
|
|
75
|
+
})
|
|
76
|
+
.onEnd(() => {
|
|
77
|
+
savedX.value = translateX.value
|
|
78
|
+
savedY.value = translateY.value
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const doubleTap = Gesture.Tap()
|
|
82
|
+
.numberOfTaps(2)
|
|
83
|
+
.onEnd(() => {
|
|
84
|
+
if (scale.value > 1) {
|
|
85
|
+
reset()
|
|
86
|
+
} else {
|
|
87
|
+
scale.value = withTiming(DOUBLE_TAP_SCALE)
|
|
88
|
+
savedScale.value = DOUBLE_TAP_SCALE
|
|
89
|
+
runOnJS(reportZoom)(true)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const composed = Gesture.Exclusive(doubleTap, Gesture.Simultaneous(pinch, pan))
|
|
94
|
+
|
|
95
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
96
|
+
transform: [
|
|
97
|
+
{ translateX: translateX.value },
|
|
98
|
+
{ translateY: translateY.value },
|
|
99
|
+
{ scale: scale.value },
|
|
100
|
+
],
|
|
101
|
+
}))
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<GestureDetector gesture={composed}>
|
|
105
|
+
<Animated.View style={[{ width, height }, styles.imageWrap]}>
|
|
106
|
+
<Animated.Image
|
|
107
|
+
source={source}
|
|
108
|
+
style={[{ width, height }, animatedStyle]}
|
|
109
|
+
resizeMode="contain"
|
|
110
|
+
/>
|
|
111
|
+
</Animated.View>
|
|
112
|
+
</GestureDetector>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ImageViewerProps {
|
|
117
|
+
/** Images to show — URI strings via `{ uri }` or `require()` sources. */
|
|
118
|
+
images: ImageSourcePropType[]
|
|
119
|
+
visible: boolean
|
|
120
|
+
onClose: () => void
|
|
121
|
+
/** Page to open on. Defaults to 0. */
|
|
122
|
+
initialIndex?: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Full-screen zoomable image gallery. Horizontal paging + pinch / double-tap
|
|
127
|
+
* zoom + pan. Page dots and a close button overlay the images.
|
|
128
|
+
*
|
|
129
|
+
* Requires `react-native-gesture-handler` (already a peer dependency).
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* <ImageViewer images={pages} visible={open} initialIndex={page} onClose={() => setOpen(false)} />
|
|
133
|
+
*/
|
|
134
|
+
export function ImageViewer({ images, visible, onClose, initialIndex = 0 }: ImageViewerProps) {
|
|
135
|
+
const { width, height } = useWindowDimensions()
|
|
136
|
+
const insets = useSafeAreaInsets()
|
|
137
|
+
const [index, setIndex] = useState(initialIndex)
|
|
138
|
+
const [pagingEnabled, setPagingEnabled] = useState(true)
|
|
139
|
+
const scrollRef = React.useRef<ScrollView>(null)
|
|
140
|
+
|
|
141
|
+
// Reset to the requested page each time the viewer is opened. State updates are
|
|
142
|
+
// deferred to the next frame (also when contentOffset must land on the page),
|
|
143
|
+
// so this never sets state synchronously during the effect.
|
|
144
|
+
React.useEffect(() => {
|
|
145
|
+
if (!visible) return
|
|
146
|
+
const handle = requestAnimationFrame(() => {
|
|
147
|
+
setIndex(initialIndex)
|
|
148
|
+
setPagingEnabled(true)
|
|
149
|
+
scrollRef.current?.scrollTo({ x: initialIndex * width, animated: false })
|
|
150
|
+
})
|
|
151
|
+
return () => cancelAnimationFrame(handle)
|
|
152
|
+
}, [visible, initialIndex, width])
|
|
153
|
+
|
|
154
|
+
// Swipe-down-to-dismiss. Only active when no image is zoomed (pagingEnabled).
|
|
155
|
+
// Drags the whole gallery down + fades the black backdrop; releases past
|
|
156
|
+
// threshold → close, otherwise springs back.
|
|
157
|
+
const dragY = useSharedValue(0)
|
|
158
|
+
const DISMISS_THRESHOLD = height * 0.18
|
|
159
|
+
|
|
160
|
+
const closeViewer = useCallback(() => onClose(), [onClose])
|
|
161
|
+
|
|
162
|
+
const swipeDown = Gesture.Pan()
|
|
163
|
+
.enabled(pagingEnabled)
|
|
164
|
+
.activeOffsetY(12)
|
|
165
|
+
.failOffsetX([-16, 16])
|
|
166
|
+
.onUpdate((e) => {
|
|
167
|
+
dragY.value = Math.max(0, e.translationY)
|
|
168
|
+
})
|
|
169
|
+
.onEnd((e) => {
|
|
170
|
+
if (e.translationY > DISMISS_THRESHOLD || e.velocityY > 800) {
|
|
171
|
+
runOnJS(closeViewer)()
|
|
172
|
+
} else {
|
|
173
|
+
dragY.value = withTiming(0)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Reset drag offset whenever the viewer opens.
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
if (visible) dragY.value = 0
|
|
180
|
+
}, [visible, dragY])
|
|
181
|
+
|
|
182
|
+
const dismissStyle = useAnimatedStyle(() => ({
|
|
183
|
+
transform: [{ translateY: dragY.value }],
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
const backdropStyle = useAnimatedStyle(() => ({
|
|
187
|
+
opacity: 1 - Math.min(dragY.value / (height * 0.5), 0.85),
|
|
188
|
+
}))
|
|
189
|
+
|
|
190
|
+
const onMomentumEnd = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
191
|
+
const page = Math.round(e.nativeEvent.contentOffset.x / width)
|
|
192
|
+
setIndex(page)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const goTo = (page: number) => {
|
|
196
|
+
scrollRef.current?.scrollTo({ x: page * width, animated: true })
|
|
197
|
+
setIndex(page)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Modal visible={visible} transparent={false} animationType="fade" onRequestClose={onClose} statusBarTranslucent>
|
|
202
|
+
<GestureHandlerRootView style={styles.root}>
|
|
203
|
+
<Animated.View style={[styles.backdrop, backdropStyle]} pointerEvents="none" />
|
|
204
|
+
<Animated.View style={[styles.container, dismissStyle]}>
|
|
205
|
+
<GestureDetector gesture={swipeDown}>
|
|
206
|
+
<Animated.View style={styles.root}>
|
|
207
|
+
<ScrollView
|
|
208
|
+
ref={scrollRef}
|
|
209
|
+
horizontal
|
|
210
|
+
pagingEnabled
|
|
211
|
+
scrollEnabled={pagingEnabled}
|
|
212
|
+
showsHorizontalScrollIndicator={false}
|
|
213
|
+
onMomentumScrollEnd={onMomentumEnd}
|
|
214
|
+
bounces={false}
|
|
215
|
+
>
|
|
216
|
+
{images.map((source, i) => (
|
|
217
|
+
<ZoomableImage
|
|
218
|
+
key={i}
|
|
219
|
+
source={source}
|
|
220
|
+
width={width}
|
|
221
|
+
height={height}
|
|
222
|
+
onZoomChange={(zoomed) => setPagingEnabled(!zoomed)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</ScrollView>
|
|
226
|
+
</Animated.View>
|
|
227
|
+
</GestureDetector>
|
|
228
|
+
|
|
229
|
+
<TouchableOpacity
|
|
230
|
+
style={[styles.closeButton, { top: insets.top + vs(8) }]}
|
|
231
|
+
onPress={onClose}
|
|
232
|
+
activeOpacity={0.7}
|
|
233
|
+
touchSoundDisabled={true}
|
|
234
|
+
accessibilityRole="button"
|
|
235
|
+
accessibilityLabel="Close"
|
|
236
|
+
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
|
237
|
+
>
|
|
238
|
+
{renderIcon('x', 26, '#fff')}
|
|
239
|
+
</TouchableOpacity>
|
|
240
|
+
|
|
241
|
+
{images.length > 1 ? (
|
|
242
|
+
<View style={[styles.dots, { bottom: insets.bottom + vs(16) }]} pointerEvents="box-none">
|
|
243
|
+
<PagerDots
|
|
244
|
+
count={images.length}
|
|
245
|
+
activeIndex={index}
|
|
246
|
+
onDotPress={goTo}
|
|
247
|
+
activeColor="#fff"
|
|
248
|
+
inactiveColor="rgba(255,255,255,0.4)"
|
|
249
|
+
/>
|
|
250
|
+
</View>
|
|
251
|
+
) : null}
|
|
252
|
+
</Animated.View>
|
|
253
|
+
</GestureHandlerRootView>
|
|
254
|
+
</Modal>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const styles = StyleSheet.create({
|
|
259
|
+
root: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
},
|
|
262
|
+
container: {
|
|
263
|
+
flex: 1,
|
|
264
|
+
},
|
|
265
|
+
backdrop: {
|
|
266
|
+
...StyleSheet.absoluteFillObject,
|
|
267
|
+
backgroundColor: '#000',
|
|
268
|
+
},
|
|
269
|
+
imageWrap: {
|
|
270
|
+
alignItems: 'center',
|
|
271
|
+
justifyContent: 'center',
|
|
272
|
+
overflow: 'hidden',
|
|
273
|
+
},
|
|
274
|
+
closeButton: {
|
|
275
|
+
position: 'absolute',
|
|
276
|
+
right: s(12),
|
|
277
|
+
width: s(40),
|
|
278
|
+
height: s(40),
|
|
279
|
+
borderRadius: s(20),
|
|
280
|
+
backgroundColor: 'rgba(0,0,0,0.4)',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
justifyContent: 'center',
|
|
283
|
+
},
|
|
284
|
+
dots: {
|
|
285
|
+
position: 'absolute',
|
|
286
|
+
left: 0,
|
|
287
|
+
right: 0,
|
|
288
|
+
alignItems: 'center',
|
|
289
|
+
},
|
|
290
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ImageViewer'
|
|
@@ -3,6 +3,7 @@ import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, TextStyle
|
|
|
3
3
|
import Animated, {
|
|
4
4
|
useAnimatedStyle,
|
|
5
5
|
interpolateColor,
|
|
6
|
+
interpolate,
|
|
6
7
|
} from 'react-native-reanimated'
|
|
7
8
|
import { AntDesign } from '@expo/vector-icons'
|
|
8
9
|
import { useTheme } from '../../theme'
|
|
@@ -11,46 +12,26 @@ import { renderIcon } from '../../utils/icons'
|
|
|
11
12
|
import { useColorTransition } from '../../utils/useColorTransition'
|
|
12
13
|
import { TIMINGS } from '../../utils/animations'
|
|
13
14
|
|
|
14
|
-
const webInputResetStyle:
|
|
15
|
+
const webInputResetStyle: Record<string, unknown> =
|
|
15
16
|
Platform.OS === 'web'
|
|
16
17
|
? { outlineStyle: 'none', outlineWidth: 0, outlineColor: 'transparent', boxShadow: 'none' }
|
|
17
18
|
: {}
|
|
18
19
|
|
|
19
20
|
export interface InputProps extends TextInputProps {
|
|
20
21
|
label?: string
|
|
21
|
-
/** Red helper text below the input; also changes border to `destructive` color. Takes priority over `hint`. */
|
|
22
22
|
error?: string
|
|
23
|
-
/** Helper text shown below the input when there is no error. */
|
|
24
23
|
hint?: string
|
|
25
|
-
/** Disabled visual state — dimmed appearance, not editable. Also sets `editable={false}`. */
|
|
26
24
|
disabled?: boolean
|
|
27
|
-
/** Text or component rendered before the input text. */
|
|
28
25
|
prefix?: React.ReactNode
|
|
29
|
-
/** Text or component rendered after the input text. */
|
|
30
26
|
suffix?: React.ReactNode
|
|
31
|
-
/** Style applied to prefix text if prefix is a string. */
|
|
32
27
|
prefixStyle?: TextStyle
|
|
33
|
-
/** Style applied to suffix text if suffix is a string. */
|
|
34
28
|
suffixStyle?: TextStyle
|
|
35
|
-
/**
|
|
36
|
-
* Icon name from `@expo/vector-icons` rendered before the input text.
|
|
37
|
-
* See https://icons.expo.fyi. Takes precedence over `prefix`.
|
|
38
|
-
*/
|
|
39
29
|
prefixIcon?: string
|
|
40
|
-
/**
|
|
41
|
-
* Icon name from `@expo/vector-icons` rendered after the input text.
|
|
42
|
-
* See https://icons.expo.fyi. Takes precedence over `suffix` (unless `type="password"`).
|
|
43
|
-
*/
|
|
44
30
|
suffixIcon?: string
|
|
45
|
-
/** Override the resolved prefix icon color. Defaults to `mutedForeground`. */
|
|
46
31
|
prefixIconColor?: string
|
|
47
|
-
/** Override the resolved suffix icon color. Defaults to `mutedForeground`. */
|
|
48
32
|
suffixIconColor?: string
|
|
49
|
-
/** Input type. When set to \`'password'\`, shows a toggle button to reveal/hide text. */
|
|
50
33
|
type?: 'text' | 'password'
|
|
51
|
-
/** Style for the outer container \`View\`. Use \`style\` (from \`TextInputProps\`) to style the \`TextInput\` itself. */
|
|
52
34
|
containerStyle?: ViewStyle
|
|
53
|
-
/** Style for the inner border wrapper (overrides padding, etc). */
|
|
54
35
|
inputWrapperStyle?: ViewStyle
|
|
55
36
|
}
|
|
56
37
|
|
|
@@ -59,7 +40,6 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
59
40
|
const [focused, setFocused] = useState(false)
|
|
60
41
|
const [showPassword, setShowPassword] = useState(false)
|
|
61
42
|
|
|
62
|
-
// Asymmetric durations — focus snaps in, blurs out subtly. Runs on UI thread.
|
|
63
43
|
const focusProgress = useColorTransition(focused, {
|
|
64
44
|
duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
|
|
65
45
|
})
|
|
@@ -75,6 +55,8 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
75
55
|
const effectiveSuffix: React.ReactNode = isPassword && !suffix && !suffixIcon ? (
|
|
76
56
|
<TouchableOpacity
|
|
77
57
|
onPress={() => setShowPassword(!showPassword)}
|
|
58
|
+
// AUDIT FIX: was padding: s(4) → ~28px target (below 44px minimum).
|
|
59
|
+
// padding: s(12) with negative margin keeps visual size but expands hit area.
|
|
78
60
|
style={styles.passwordToggle}
|
|
79
61
|
activeOpacity={0.6}
|
|
80
62
|
accessibilityRole="button"
|
|
@@ -86,10 +68,16 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
86
68
|
? renderIcon(suffixIcon, 20, suffixIconColor ?? colors.foregroundMuted)
|
|
87
69
|
: suffix
|
|
88
70
|
|
|
89
|
-
|
|
71
|
+
// Border drawn on an absolute overlay so the 1px→2px weight change never
|
|
72
|
+
// resizes the layout box (which would reflow content / shift the interface).
|
|
73
|
+
// Wrapper keeps borderWidth: 0; overlay grows inward and is non-interactive.
|
|
74
|
+
const borderAnimStyle = useAnimatedStyle(() => ({
|
|
90
75
|
borderColor: error
|
|
91
76
|
? colors.destructive
|
|
92
77
|
: interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
|
|
78
|
+
borderWidth: error
|
|
79
|
+
? 2
|
|
80
|
+
: interpolate(focusProgress.value, [0, 1], [1, 2]),
|
|
93
81
|
}))
|
|
94
82
|
|
|
95
83
|
return (
|
|
@@ -99,10 +87,10 @@ export function Input({ label, error, hint, disabled, prefix, suffix, prefixStyl
|
|
|
99
87
|
style={[
|
|
100
88
|
styles.inputWrapper,
|
|
101
89
|
{ backgroundColor: isDisabled ? colors.surface : colors.background },
|
|
102
|
-
borderColorStyle,
|
|
103
90
|
inputWrapperStyle,
|
|
104
91
|
]}
|
|
105
92
|
>
|
|
93
|
+
<Animated.View style={[styles.borderOverlay, borderAnimStyle]} pointerEvents="none" />
|
|
106
94
|
{effectivePrefix ? (
|
|
107
95
|
typeof effectivePrefix === 'string' ? (
|
|
108
96
|
<Text style={[styles.prefixText, { color: colors.foregroundMuted }, prefixStyle]} allowFontScaling={true}>
|
|
@@ -168,20 +156,25 @@ const styles = StyleSheet.create({
|
|
|
168
156
|
opacity: 0.6,
|
|
169
157
|
},
|
|
170
158
|
label: {
|
|
171
|
-
fontFamily: '
|
|
159
|
+
fontFamily: 'Sohne-Medium',
|
|
172
160
|
fontSize: ms(14),
|
|
173
161
|
},
|
|
174
162
|
inputWrapper: {
|
|
175
163
|
flexDirection: 'row',
|
|
176
164
|
alignItems: 'center',
|
|
177
|
-
|
|
165
|
+
// Border lives on borderOverlay (absolute) so its 1px→2px focus change
|
|
166
|
+
// never resizes this box. Wrapper itself carries no border.
|
|
178
167
|
borderRadius: 8,
|
|
179
168
|
paddingHorizontal: s(14),
|
|
180
169
|
paddingVertical: vs(11),
|
|
181
170
|
minHeight: 48,
|
|
182
171
|
},
|
|
172
|
+
borderOverlay: {
|
|
173
|
+
...StyleSheet.absoluteFillObject,
|
|
174
|
+
borderRadius: 8,
|
|
175
|
+
},
|
|
183
176
|
input: {
|
|
184
|
-
fontFamily: '
|
|
177
|
+
fontFamily: 'Sohne-Regular',
|
|
185
178
|
flex: 1,
|
|
186
179
|
fontSize: ms(16),
|
|
187
180
|
paddingVertical: vs(2),
|
|
@@ -191,7 +184,7 @@ const styles = StyleSheet.create({
|
|
|
191
184
|
marginRight: s(8),
|
|
192
185
|
},
|
|
193
186
|
prefixText: {
|
|
194
|
-
fontFamily: '
|
|
187
|
+
fontFamily: 'Sohne-Regular',
|
|
195
188
|
fontSize: ms(15),
|
|
196
189
|
marginRight: s(8),
|
|
197
190
|
},
|
|
@@ -199,15 +192,18 @@ const styles = StyleSheet.create({
|
|
|
199
192
|
marginLeft: s(8),
|
|
200
193
|
},
|
|
201
194
|
suffixText: {
|
|
202
|
-
fontFamily: '
|
|
195
|
+
fontFamily: 'Sohne-Regular',
|
|
203
196
|
fontSize: ms(15),
|
|
204
197
|
marginLeft: s(8),
|
|
205
198
|
},
|
|
199
|
+
// AUDIT FIX: was padding: s(4) → ~28px tap target. Now 12px padding → ~44px.
|
|
200
|
+
// Negative margin compensates so the visual icon position is unchanged.
|
|
206
201
|
passwordToggle: {
|
|
207
|
-
padding: s(
|
|
202
|
+
padding: s(12),
|
|
203
|
+
margin: -s(8),
|
|
208
204
|
},
|
|
209
205
|
helperText: {
|
|
210
|
-
fontFamily: '
|
|
206
|
+
fontFamily: 'Sohne-Regular',
|
|
211
207
|
fontSize: ms(13),
|
|
212
208
|
},
|
|
213
209
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
|
-
import { s,
|
|
4
|
+
import { s, ms, mvs } from '../../utils/scaling'
|
|
5
5
|
import { renderIcon } from '../../utils/icons'
|
|
6
6
|
|
|
7
7
|
export interface LabelValueProps {
|
|
@@ -14,7 +14,7 @@ export interface LabelValueProps {
|
|
|
14
14
|
style?: ViewStyle
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function LabelValueBase({ label, value, iconName, iconColor, style }: LabelValueProps) {
|
|
18
18
|
const { colors } = useTheme()
|
|
19
19
|
|
|
20
20
|
const resolvedIcon = iconName
|
|
@@ -40,6 +40,8 @@ export function LabelValue({ label, value, iconName, iconColor, style }: LabelVa
|
|
|
40
40
|
)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export const LabelValue = React.memo(LabelValueBase)
|
|
44
|
+
|
|
43
45
|
const styles = StyleSheet.create({
|
|
44
46
|
container: {
|
|
45
47
|
flexDirection: 'row',
|
|
@@ -57,12 +59,12 @@ const styles = StyleSheet.create({
|
|
|
57
59
|
justifyContent: 'center',
|
|
58
60
|
},
|
|
59
61
|
label: {
|
|
60
|
-
fontFamily: '
|
|
62
|
+
fontFamily: 'Sohne-Regular',
|
|
61
63
|
fontSize: ms(13),
|
|
62
64
|
lineHeight: mvs(18),
|
|
63
65
|
},
|
|
64
66
|
value: {
|
|
65
|
-
fontFamily: '
|
|
67
|
+
fontFamily: 'Sohne-Medium',
|
|
66
68
|
fontSize: ms(14),
|
|
67
69
|
lineHeight: mvs(20),
|
|
68
70
|
textAlign: 'right',
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
+
import { useTheme } from '../../theme'
|
|
4
|
+
import { s, vs } from '../../utils/scaling'
|
|
5
|
+
import { RADIUS } from '../../tokens'
|
|
6
|
+
|
|
7
|
+
export type ListGroupVariant = 'plain' | 'card'
|
|
8
|
+
|
|
9
|
+
export interface ListGroupProps {
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
/**
|
|
12
|
+
* - `plain` (default): no background, plain ListItems inside.
|
|
13
|
+
* - `card`: card surface with background + border wrapping plain ListItems.
|
|
14
|
+
*/
|
|
15
|
+
variant?: ListGroupVariant
|
|
16
|
+
style?: ViewStyle
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ListGroupHeaderProps {
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
style?: ViewStyle
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ListGroupFooterProps {
|
|
25
|
+
children: React.ReactNode
|
|
26
|
+
style?: ViewStyle
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ListGroup wraps multiple ListItems and auto-adds separators between them.
|
|
31
|
+
* Use variant="card" for a standalone surface or "plain" for items inside another container.
|
|
32
|
+
*/
|
|
33
|
+
export function ListGroup({ children, variant = 'plain', style }: ListGroupProps) {
|
|
34
|
+
const { colors } = useTheme()
|
|
35
|
+
|
|
36
|
+
// Auto-inject showSeparator={true} to all ListItem children except the last
|
|
37
|
+
const processedChildren = React.Children.map(children, (child, index) => {
|
|
38
|
+
if (!React.isValidElement(child)) return child
|
|
39
|
+
|
|
40
|
+
// Skip ListGroup.Header and ListGroup.Footer
|
|
41
|
+
if (child.type === ListGroupHeader || child.type === ListGroupFooter) {
|
|
42
|
+
return child
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if it's a ListItem (has title prop as a heuristic)
|
|
46
|
+
const childProps = child.props as Record<string, unknown>
|
|
47
|
+
const isListItem = 'title' in childProps
|
|
48
|
+
if (!isListItem) return child
|
|
49
|
+
|
|
50
|
+
const isLast = index === React.Children.count(children) - 1
|
|
51
|
+
|
|
52
|
+
// Only add separator if not already explicitly set and not last item
|
|
53
|
+
if (childProps['showSeparator'] === undefined && !isLast) {
|
|
54
|
+
return React.cloneElement(child as React.ReactElement<Record<string, unknown>>, {
|
|
55
|
+
showSeparator: true,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return child
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const cardStyle: ViewStyle =
|
|
63
|
+
variant === 'card'
|
|
64
|
+
? {
|
|
65
|
+
backgroundColor: colors.card,
|
|
66
|
+
borderRadius: RADIUS.md,
|
|
67
|
+
borderWidth: 1,
|
|
68
|
+
borderColor: colors.border,
|
|
69
|
+
shadowColor: '#000',
|
|
70
|
+
shadowOffset: { width: 0, height: 2 },
|
|
71
|
+
shadowOpacity: 0.06,
|
|
72
|
+
shadowRadius: 6,
|
|
73
|
+
elevation: 2,
|
|
74
|
+
paddingVertical: vs(4),
|
|
75
|
+
}
|
|
76
|
+
: {}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<View style={[styles.container, cardStyle, style]}>
|
|
80
|
+
{processedChildren}
|
|
81
|
+
</View>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function ListGroupHeader({ children, style }: ListGroupHeaderProps) {
|
|
86
|
+
const { colors } = useTheme()
|
|
87
|
+
|
|
88
|
+
if (typeof children === 'string') {
|
|
89
|
+
return (
|
|
90
|
+
<View style={[styles.header, { borderBottomColor: colors.separator }, style]}>
|
|
91
|
+
<Text style={[styles.headerText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
92
|
+
{children}
|
|
93
|
+
</Text>
|
|
94
|
+
</View>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <View style={[styles.header, { borderBottomColor: colors.separator }, style]}>{children}</View>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function ListGroupFooter({ children, style }: ListGroupFooterProps) {
|
|
102
|
+
const { colors } = useTheme()
|
|
103
|
+
|
|
104
|
+
if (typeof children === 'string') {
|
|
105
|
+
return (
|
|
106
|
+
<View style={[styles.footer, style]}>
|
|
107
|
+
<Text style={[styles.footerText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
|
|
108
|
+
{children}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return <View style={[styles.footer, style]}>{children}</View>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
ListGroup.Header = ListGroupHeader
|
|
118
|
+
ListGroup.Footer = ListGroupFooter
|
|
119
|
+
|
|
120
|
+
const styles = StyleSheet.create({
|
|
121
|
+
container: {
|
|
122
|
+
overflow: 'hidden',
|
|
123
|
+
},
|
|
124
|
+
header: {
|
|
125
|
+
paddingHorizontal: s(16),
|
|
126
|
+
paddingTop: vs(12),
|
|
127
|
+
paddingBottom: vs(8),
|
|
128
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
129
|
+
},
|
|
130
|
+
headerText: {
|
|
131
|
+
fontFamily: 'Sohne-SemiBold',
|
|
132
|
+
fontSize: 13,
|
|
133
|
+
letterSpacing: 0.32,
|
|
134
|
+
textTransform: 'uppercase',
|
|
135
|
+
},
|
|
136
|
+
footer: {
|
|
137
|
+
paddingHorizontal: s(16),
|
|
138
|
+
paddingTop: vs(8),
|
|
139
|
+
paddingBottom: vs(12),
|
|
140
|
+
},
|
|
141
|
+
footerText: {
|
|
142
|
+
fontFamily: 'Sohne-Regular',
|
|
143
|
+
fontSize: 12,
|
|
144
|
+
},
|
|
145
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ListGroup'
|