@retray-dev/ui-kit 10.0.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/COMPONENTS.md +150 -17
  2. package/CONSUMER.md +1 -1
  3. package/README.md +4 -4
  4. package/dist/Accordion.d.mts +1 -1
  5. package/dist/Accordion.d.ts +1 -1
  6. package/dist/Accordion.js +3 -3
  7. package/dist/Accordion.mjs +2 -2
  8. package/dist/AlertBanner.js +1 -1
  9. package/dist/AlertBanner.mjs +2 -2
  10. package/dist/AppHeader.js +1 -1
  11. package/dist/AppHeader.mjs +3 -3
  12. package/dist/Badge.js +1 -1
  13. package/dist/Badge.mjs +2 -2
  14. package/dist/Button.js +1 -1
  15. package/dist/Button.mjs +2 -2
  16. package/dist/CategoryStrip.js +1 -1
  17. package/dist/CategoryStrip.mjs +2 -2
  18. package/dist/Chip.js +1 -1
  19. package/dist/Chip.mjs +2 -2
  20. package/dist/ConfirmDialog.d.mts +6 -1
  21. package/dist/ConfirmDialog.d.ts +6 -1
  22. package/dist/ConfirmDialog.js +45 -15
  23. package/dist/ConfirmDialog.mjs +3 -3
  24. package/dist/CurrencyInput.js +1 -1
  25. package/dist/CurrencyInput.mjs +3 -3
  26. package/dist/DetailRow.d.mts +1 -1
  27. package/dist/DetailRow.d.ts +1 -1
  28. package/dist/DetailRow.js +1 -1
  29. package/dist/DetailRow.mjs +2 -2
  30. package/dist/EmptyState.js +1 -1
  31. package/dist/EmptyState.mjs +3 -3
  32. package/dist/ErrorBoundary.js +1 -1
  33. package/dist/ErrorBoundary.mjs +2 -2
  34. package/dist/IconButton.js +1 -1
  35. package/dist/IconButton.mjs +2 -2
  36. package/dist/IconPicker.d.mts +17 -0
  37. package/dist/IconPicker.d.ts +17 -0
  38. package/dist/IconPicker.js +997 -0
  39. package/dist/IconPicker.mjs +7 -0
  40. package/dist/ImageUpload.d.mts +3 -1
  41. package/dist/ImageUpload.d.ts +3 -1
  42. package/dist/ImageUpload.js +28 -10
  43. package/dist/ImageUpload.mjs +1 -1
  44. package/dist/ImageViewer.js +282 -141
  45. package/dist/ImageViewer.mjs +5 -3
  46. package/dist/Input.js +1 -1
  47. package/dist/Input.mjs +2 -2
  48. package/dist/LabelValue.js +1 -1
  49. package/dist/LabelValue.mjs +2 -2
  50. package/dist/ListItem.js +1 -1
  51. package/dist/ListItem.mjs +2 -2
  52. package/dist/MediaCard.js +1 -1
  53. package/dist/MediaCard.mjs +2 -2
  54. package/dist/MenuItem.js +1 -1
  55. package/dist/MenuItem.mjs +2 -2
  56. package/dist/NumberStepper.d.mts +19 -0
  57. package/dist/NumberStepper.d.ts +19 -0
  58. package/dist/NumberStepper.js +410 -0
  59. package/dist/NumberStepper.mjs +9 -0
  60. package/dist/PagerDots.js +1 -1
  61. package/dist/PagerDots.mjs +2 -2
  62. package/dist/PricingCard.js +1 -1
  63. package/dist/PricingCard.mjs +4 -4
  64. package/dist/SelectableGrid.js +1 -1
  65. package/dist/SelectableGrid.mjs +2 -2
  66. package/dist/Sheet.js +16 -13
  67. package/dist/Sheet.mjs +1 -1
  68. package/dist/SheetSelect.js +1 -1
  69. package/dist/SheetSelect.mjs +2 -2
  70. package/dist/Switch.js +40 -17
  71. package/dist/Switch.mjs +1 -1
  72. package/dist/TabBar.js +1 -1
  73. package/dist/TabBar.mjs +2 -2
  74. package/dist/Textarea.js +1 -1
  75. package/dist/Textarea.mjs +2 -2
  76. package/dist/Toggle.js +1 -1
  77. package/dist/Toggle.mjs +2 -2
  78. package/dist/chunk-53Z3NYGE.mjs +742 -0
  79. package/dist/{chunk-VQ57HWPL.mjs → chunk-6L4G6PBT.mjs} +1 -1
  80. package/dist/{chunk-6OAZJ577.mjs → chunk-6SECQ2ZF.mjs} +2 -2
  81. package/dist/{chunk-KIHCWCWL.mjs → chunk-7LWRKMF5.mjs} +1 -1
  82. package/dist/{chunk-4I7D47FH.mjs → chunk-AJRVDP2H.mjs} +3 -3
  83. package/dist/{chunk-6MKGPAR2.mjs → chunk-BEMIQXXU.mjs} +1 -1
  84. package/dist/chunk-BUMAMSTZ.mjs +126 -0
  85. package/dist/{chunk-UREA2GYY.mjs → chunk-DYT7BG5I.mjs} +1 -1
  86. package/dist/{chunk-Z4BVUWW6.mjs → chunk-ELXBDILQ.mjs} +20 -32
  87. package/dist/{chunk-A4MDAP7G.mjs → chunk-FCSSQK3L.mjs} +1 -1
  88. package/dist/{chunk-2TFTAWVJ.mjs → chunk-HTHGSXFG.mjs} +1 -1
  89. package/dist/{chunk-VGTDN7SW.mjs → chunk-IX3NYLYQ.mjs} +1 -1
  90. package/dist/{chunk-T7XZ7H7Y.mjs → chunk-KA7LTET3.mjs} +17 -3
  91. package/dist/{chunk-URI2WBIV.mjs → chunk-KOO4WITD.mjs} +1 -1
  92. package/dist/{chunk-JUXSWN54.mjs → chunk-NMU5FMQJ.mjs} +1 -1
  93. package/dist/{chunk-LXJIIOYQ.mjs → chunk-RYZC432S.mjs} +1 -1
  94. package/dist/{chunk-JB67UOB5.mjs → chunk-S2R7UVOE.mjs} +1 -1
  95. package/dist/{chunk-ZUR7AU5R.mjs → chunk-SXLKNTA4.mjs} +1 -1
  96. package/dist/{chunk-3U4SSNWP.mjs → chunk-T2KCAHOS.mjs} +1 -1
  97. package/dist/{chunk-ZJKGQMYH.mjs → chunk-TB6SD2FT.mjs} +1 -1
  98. package/dist/{chunk-AZJF2BLK.mjs → chunk-TBNZHU6C.mjs} +1 -1
  99. package/dist/{chunk-Y4GL2MHX.mjs → chunk-TZDGAP5N.mjs} +28 -10
  100. package/dist/{chunk-CZCQZHG6.mjs → chunk-U2XJFYED.mjs} +1 -1
  101. package/dist/{chunk-TERDKCLE.mjs → chunk-VF2ATYN3.mjs} +1 -1
  102. package/dist/{chunk-OHBNABL5.mjs → chunk-VKID2D2I.mjs} +1 -1
  103. package/dist/{chunk-QKH5ZOD5.mjs → chunk-WF2XDFRK.mjs} +40 -17
  104. package/dist/{chunk-FZZLPJ6B.mjs → chunk-WYEUNUTP.mjs} +44 -15
  105. package/dist/{chunk-PFZTM6D5.mjs → chunk-Y2NS74WS.mjs} +9 -7
  106. package/dist/{chunk-O3HA6TYM.mjs → chunk-YJ7I257J.mjs} +3 -3
  107. package/dist/{chunk-NA7PARID.mjs → chunk-Z4VHZ7B5.mjs} +1 -1
  108. package/dist/{chunk-MLF3EZFW.mjs → chunk-Z6SFHN6T.mjs} +1 -1
  109. package/dist/{chunk-4K625MVM.mjs → chunk-ZZ2R6KZ3.mjs} +1 -1
  110. package/dist/index.d.mts +4 -1
  111. package/dist/index.d.ts +4 -1
  112. package/dist/index.js +1011 -88
  113. package/dist/index.mjs +34 -32
  114. package/package.json +1 -1
  115. package/src/components/Accordion/Accordion.tsx +7 -3
  116. package/src/components/ConfirmDialog/ConfirmDialog.tsx +61 -23
  117. package/src/components/DetailRow/DetailRow.tsx +1 -1
  118. package/src/components/IconPicker/IconPicker.tsx +383 -0
  119. package/src/components/IconPicker/index.ts +1 -0
  120. package/src/components/ImageUpload/ImageUpload.tsx +34 -12
  121. package/src/components/ImageViewer/ImageViewer.tsx +25 -30
  122. package/src/components/NumberStepper/NumberStepper.tsx +147 -0
  123. package/src/components/NumberStepper/index.ts +1 -0
  124. package/src/components/Sheet/Sheet.tsx +10 -9
  125. package/src/components/Switch/Switch.tsx +30 -17
  126. package/src/index.ts +3 -1
  127. package/src/utils/curatedIcons.ts +286 -0
  128. package/src/utils/icons.ts +20 -2
package/dist/index.mjs CHANGED
@@ -1,60 +1,62 @@
1
+ export { Switch } from './chunk-WF2XDFRK.mjs';
2
+ export { TabBar } from './chunk-Z6SFHN6T.mjs';
1
3
  export { Tabs, TabsContent } from './chunk-GQYFLP3D.mjs';
2
4
  export { Text } from './chunk-WJLKJMKR.mjs';
3
- export { Textarea } from './chunk-CZCQZHG6.mjs';
4
- export { Toggle } from './chunk-KIHCWCWL.mjs';
5
+ export { Textarea } from './chunk-U2XJFYED.mjs';
6
+ export { Toggle } from './chunk-7LWRKMF5.mjs';
5
7
  export { VirtualList } from './chunk-NC5ZTR2Y.mjs';
8
+ export { Select } from './chunk-A3A6KNQN.mjs';
9
+ export { SelectableGrid } from './chunk-Z4VHZ7B5.mjs';
6
10
  export { Separator } from './chunk-MX6HRKMI.mjs';
7
- export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-PFZTM6D5.mjs';
8
- export { SheetSelect } from './chunk-URI2WBIV.mjs';
11
+ export { BottomSheetModalProvider, Sheet, BottomSheetTextInput as SheetTextInput } from './chunk-Y2NS74WS.mjs';
12
+ export { SheetSelect } from './chunk-KOO4WITD.mjs';
9
13
  export { Skeleton } from './chunk-AJ7ZDNBT.mjs';
10
14
  export { Slider } from './chunk-JMOZEC77.mjs';
11
- export { Switch } from './chunk-QKH5ZOD5.mjs';
12
- export { TabBar } from './chunk-MLF3EZFW.mjs';
15
+ export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-GD6KXMG5.mjs';
16
+ export { NumberStepper } from './chunk-BUMAMSTZ.mjs';
13
17
  export { Pressable } from './chunk-MBMXYJJV.mjs';
14
- export { PricingCard } from './chunk-4I7D47FH.mjs';
18
+ export { PricingCard } from './chunk-AJRVDP2H.mjs';
15
19
  export { Progress } from './chunk-OB4JUQ3O.mjs';
16
20
  export { RadioGroup } from './chunk-X4G6APW6.mjs';
17
21
  export { RetrayProvider } from './chunk-YFZ3ELX5.mjs';
18
22
  export { ToastProvider, sonnerToast as toast, useToast } from './chunk-2UYENBLV.mjs';
19
- export { Select } from './chunk-A3A6KNQN.mjs';
20
- export { SelectableGrid } from './chunk-NA7PARID.mjs';
21
- export { LabelValue } from './chunk-A4MDAP7G.mjs';
23
+ export { ImageViewer } from './chunk-ELXBDILQ.mjs';
24
+ export { PagerDots } from './chunk-ZZ2R6KZ3.mjs';
25
+ export { LabelValue } from './chunk-FCSSQK3L.mjs';
22
26
  export { ListGroup, ListGroupFooter, ListGroupHeader } from './chunk-SOA2Z4RB.mjs';
23
- export { ListItem } from './chunk-OHBNABL5.mjs';
24
- export { MediaCard } from './chunk-VGTDN7SW.mjs';
27
+ export { ListItem } from './chunk-VKID2D2I.mjs';
28
+ export { MediaCard } from './chunk-IX3NYLYQ.mjs';
25
29
  export { MenuGroup, MenuGroupFooter, MenuGroupHeader } from './chunk-IRRY3CRZ.mjs';
26
- export { MenuItem } from './chunk-ZJKGQMYH.mjs';
27
- export { MonthPicker, dateToMonthPickerValue, monthPickerValueToDate } from './chunk-GD6KXMG5.mjs';
28
- export { DetailRow } from './chunk-JB67UOB5.mjs';
29
- export { EmptyState } from './chunk-6OAZJ577.mjs';
30
- export { ErrorBoundary } from './chunk-LXJIIOYQ.mjs';
30
+ export { MenuItem } from './chunk-TB6SD2FT.mjs';
31
+ export { DetailRow } from './chunk-S2R7UVOE.mjs';
32
+ export { EmptyState } from './chunk-6SECQ2ZF.mjs';
33
+ export { ErrorBoundary } from './chunk-RYZC432S.mjs';
31
34
  export { Form, FormField, FormFooter, FormSection } from './chunk-6Q64UFIA.mjs';
32
- export { ImageUpload } from './chunk-Y4GL2MHX.mjs';
35
+ export { IconPicker } from './chunk-53Z3NYGE.mjs';
36
+ export { ImageUpload } from './chunk-TZDGAP5N.mjs';
33
37
  export { Spinner } from './chunk-WBOOUHSS.mjs';
34
- export { ImageViewer } from './chunk-Z4BVUWW6.mjs';
35
- export { PagerDots } from './chunk-4K625MVM.mjs';
36
38
  export { ButtonGroup } from './chunk-3BBOZ3OQ.mjs';
37
39
  export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from './chunk-ID72TK46.mjs';
38
- export { CategoryStrip } from './chunk-VQ57HWPL.mjs';
40
+ export { CategoryStrip } from './chunk-6L4G6PBT.mjs';
39
41
  import './chunk-YNROWHQJ.mjs';
40
42
  export { Checkbox } from './chunk-AV4EMIRH.mjs';
41
- export { Chip, ChipGroup } from './chunk-UREA2GYY.mjs';
42
- export { ConfirmDialog } from './chunk-FZZLPJ6B.mjs';
43
+ export { Chip, ChipGroup } from './chunk-DYT7BG5I.mjs';
44
+ export { ConfirmDialog } from './chunk-WYEUNUTP.mjs';
43
45
  export { CurrencyDisplay } from './chunk-BRKYVJVV.mjs';
44
- export { CurrencyInput } from './chunk-JUXSWN54.mjs';
45
- export { Input } from './chunk-ZUR7AU5R.mjs';
46
- export { Accordion } from './chunk-O3HA6TYM.mjs';
47
- export { AlertBanner } from './chunk-6MKGPAR2.mjs';
48
- export { AppHeader } from './chunk-AZJF2BLK.mjs';
49
- export { IconButton } from './chunk-3U4SSNWP.mjs';
46
+ export { CurrencyInput } from './chunk-NMU5FMQJ.mjs';
47
+ export { Input } from './chunk-SXLKNTA4.mjs';
48
+ export { Accordion } from './chunk-YJ7I257J.mjs';
49
+ export { AlertBanner } from './chunk-BEMIQXXU.mjs';
50
+ export { AppHeader } from './chunk-TBNZHU6C.mjs';
51
+ export { IconButton } from './chunk-T2KCAHOS.mjs';
50
52
  export { Avatar } from './chunk-JT7HKXRB.mjs';
51
- export { Badge } from './chunk-TERDKCLE.mjs';
52
- export { Button } from './chunk-2TFTAWVJ.mjs';
53
+ export { Badge } from './chunk-VF2ATYN3.mjs';
54
+ export { Button } from './chunk-HTHGSXFG.mjs';
53
55
  import './chunk-3DKJ2GIC.mjs';
54
56
  export { impactHeavy, impactLight, impactMedium, notificationError, notificationSuccess, notificationWarning, richHaptics, selectionAsync } from './chunk-EJ7ZPXOH.mjs';
55
57
  import './chunk-DVK4G2GT.mjs';
56
58
  export { BREAKPOINTS, ICON_SIZES, RADIUS, SHADOWS, SPACING, TYPOGRAPHY } from './chunk-QY3X2UYR.mjs';
57
- export { Icon, configureIconFamilies, renderIcon } from './chunk-T7XZ7H7Y.mjs';
59
+ export { Icon, configureIconFamilies, getValidIconNames, renderIcon } from './chunk-KA7LTET3.mjs';
58
60
  export { ThemeProvider, defaultDark, defaultLight, deriveColors, useTheme } from './chunk-SOYNZDVY.mjs';
59
61
  import './chunk-2CE3TQVY.mjs';
60
62
  import './chunk-Y6FXYEAI.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retray-dev/ui-kit",
3
- "version": "10.0.0",
3
+ "version": "10.2.0",
4
4
  "description": "Personal UI Kit for React Native / Expo",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -21,7 +21,7 @@ import { TIMINGS, EASINGS } from '../../utils/animations'
21
21
 
22
22
  export interface AccordionItem {
23
23
  value: string
24
- trigger: string
24
+ trigger: string | React.ReactNode
25
25
  content: React.ReactNode
26
26
  /** Icon name from @expo/vector-icons rendered left of trigger. */
27
27
  iconName?: string
@@ -102,11 +102,15 @@ function AccordionItemComponent({
102
102
  }}
103
103
  accessibilityRole="button"
104
104
  accessibilityState={{ expanded: isOpen }}
105
- accessibilityLabel={item.trigger}
105
+ accessibilityLabel={typeof item.trigger === 'string' ? item.trigger : undefined}
106
106
  >
107
107
  <View style={styles.triggerContent}>
108
108
  {resolvedIcon ? <View style={styles.icon}>{resolvedIcon}</View> : null}
109
- <Text style={[styles.triggerText, { color: colors.foreground }]} allowFontScaling={true}>{item.trigger}</Text>
109
+ {typeof item.trigger === 'string' ? (
110
+ <Text style={[styles.triggerText, { color: colors.foreground }]} allowFontScaling={true}>{item.trigger}</Text>
111
+ ) : (
112
+ item.trigger
113
+ )}
110
114
  </View>
111
115
  <Animated.View style={[styles.chevron, rotationStyle]}>
112
116
  <Entypo name="chevron-down" size={18} color={colors.foregroundMuted} />
@@ -1,7 +1,6 @@
1
1
  import React, { useEffect, useRef } from 'react'
2
- import { View, Text, StyleSheet } from 'react-native'
3
- import {
4
- BottomSheetModal,
2
+ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'
3
+ import BottomSheet, {
5
4
  BottomSheetView,
6
5
  BottomSheetBackdrop,
7
6
  type BottomSheetBackdropProps,
@@ -15,12 +14,17 @@ import { s, vs, ms, mvs } from '../../utils/scaling'
15
14
  export interface ConfirmDialogProps {
16
15
  visible: boolean
17
16
  title: string
17
+ /** Secondary text below title. */
18
+ subtitle?: string
19
+ /** @deprecated Use `subtitle` instead. */
18
20
  description?: string
19
21
  confirmLabel?: string
20
22
  cancelLabel?: string
21
23
  confirmVariant?: 'primary' | 'destructive'
22
24
  /** Show a loading spinner in the confirm button (e.g. while async action completes). */
23
25
  loading?: boolean
26
+ /** Show an X close button in the top-right corner. */
27
+ showCloseButton?: boolean
24
28
  onConfirm: () => void
25
29
  onCancel: () => void
26
30
  }
@@ -28,23 +32,26 @@ export interface ConfirmDialogProps {
28
32
  export function ConfirmDialog({
29
33
  visible,
30
34
  title,
35
+ subtitle,
31
36
  description,
32
37
  confirmLabel = 'Confirm',
33
38
  cancelLabel = 'Cancel',
34
39
  confirmVariant = 'primary',
35
40
  loading = false,
41
+ showCloseButton = false,
36
42
  onConfirm,
37
43
  onCancel,
38
44
  }: ConfirmDialogProps) {
39
45
  const { colors } = useTheme()
40
- const ref = useRef<BottomSheetModal>(null)
46
+ const ref = useRef<BottomSheet>(null)
47
+ const effectiveSubtitle = subtitle ?? description
41
48
 
42
49
  useEffect(() => {
43
50
  if (visible) {
44
51
  impactMedium()
45
- ref.current?.present()
52
+ ref.current?.snapToIndex(0)
46
53
  } else {
47
- ref.current?.dismiss()
54
+ ref.current?.close()
48
55
  }
49
56
  }, [visible])
50
57
 
@@ -58,24 +65,42 @@ export function ConfirmDialog({
58
65
  )
59
66
 
60
67
  return (
61
- <BottomSheetModal
68
+ <BottomSheet
62
69
  ref={ref}
70
+ index={-1}
71
+ onClose={onCancel}
63
72
  enableDynamicSizing
64
- onDismiss={onCancel}
65
73
  backdropComponent={renderBackdrop}
66
74
  backgroundStyle={[styles.background, { backgroundColor: colors.card }]}
67
75
  handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
68
76
  enablePanDownToClose
69
77
  >
70
78
  <BottomSheetView style={styles.content}>
71
- <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
72
- {title}
73
- </Text>
74
- {description ? (
75
- <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
76
- {description}
77
- </Text>
78
- ) : null}
79
+ <View style={styles.header} accessibilityRole="header">
80
+ <View style={styles.headerRow}>
81
+ <Text style={[styles.title, { color: colors.foreground }]} allowFontScaling={true}>
82
+ {title}
83
+ </Text>
84
+ {showCloseButton ? (
85
+ <TouchableOpacity
86
+ onPress={onCancel}
87
+ style={styles.closeButton}
88
+ activeOpacity={0.6}
89
+ touchSoundDisabled={true}
90
+ accessibilityRole="button"
91
+ accessibilityLabel="Close"
92
+ hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
93
+ >
94
+ <Feather name="x" size={ms(18)} color={colors.foregroundMuted} />
95
+ </TouchableOpacity>
96
+ ) : null}
97
+ </View>
98
+ {effectiveSubtitle ? (
99
+ <Text style={[styles.subtitle, { color: colors.foregroundMuted }]} allowFontScaling={true}>
100
+ {effectiveSubtitle}
101
+ </Text>
102
+ ) : null}
103
+ </View>
79
104
  <View style={styles.actions}>
80
105
  <Button
81
106
  label={confirmLabel}
@@ -105,7 +130,7 @@ export function ConfirmDialog({
105
130
  />
106
131
  </View>
107
132
  </BottomSheetView>
108
- </BottomSheetModal>
133
+ </BottomSheet>
109
134
  )
110
135
  }
111
136
 
@@ -120,19 +145,32 @@ const styles = StyleSheet.create({
120
145
  borderRadius: ms(2),
121
146
  },
122
147
  content: {
123
- paddingHorizontal: s(24),
148
+ paddingHorizontal: s(16),
124
149
  paddingBottom: vs(32),
125
- gap: vs(12),
150
+ },
151
+ header: {
152
+ paddingTop: vs(4),
153
+ paddingBottom: vs(12),
154
+ gap: vs(4),
155
+ },
156
+ headerRow: {
157
+ flexDirection: 'row',
158
+ alignItems: 'center',
159
+ justifyContent: 'space-between',
126
160
  },
127
161
  title: {
128
162
  fontFamily: 'Sohne-SemiBold',
129
163
  fontSize: ms(18),
130
- lineHeight: mvs(26),
164
+ flex: 1,
165
+ },
166
+ closeButton: {
167
+ padding: s(4),
168
+ marginLeft: s(8),
131
169
  },
132
- description: {
170
+ subtitle: {
133
171
  fontFamily: 'Sohne-Regular',
134
- fontSize: ms(15),
135
- lineHeight: mvs(22),
172
+ fontSize: ms(14),
173
+ lineHeight: mvs(20),
136
174
  },
137
175
  actions: {
138
176
  gap: vs(10),
@@ -16,7 +16,7 @@ const weightMap: Record<DetailRowLabelWeight, string> = {
16
16
 
17
17
  export interface DetailRowProps {
18
18
  label: React.ReactNode
19
- value: string | React.ReactNode
19
+ value: string | number | React.ReactNode
20
20
  /** Dotted/dashed/solid line between label and value. Defaults to 'dotted'. */
21
21
  separator?: DetailRowSeparator
22
22
  labelWeight?: DetailRowLabelWeight
@@ -0,0 +1,383 @@
1
+ import React, { useRef, useState, useCallback, useEffect, useMemo, useId } from 'react'
2
+ import { View, Text, TouchableOpacity, StyleSheet, ViewStyle, Dimensions } from 'react-native'
3
+ import { ScrollView } from 'react-native-gesture-handler'
4
+ import {
5
+ BottomSheetModal,
6
+ BottomSheetScrollView,
7
+ BottomSheetBackdrop,
8
+ } from '@gorhom/bottom-sheet'
9
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
10
+ import { useTheme } from '../../theme'
11
+ import { renderIcon } from '../../utils/icons'
12
+ import { CURATED_ICONS, ALL_CURATED_ICONS } from '../../utils/curatedIcons'
13
+ import { selectionAsync as hapticSelection, impactMedium } from '../../utils/haptics'
14
+ import { s, vs, ms } from '../../utils/scaling'
15
+ import { RADIUS } from '../../tokens'
16
+ import type { BottomSheetBackdropProps, BottomSheetModal as BottomSheetModalType } from '@gorhom/bottom-sheet'
17
+
18
+ const NUM_COLUMNS = 6
19
+ const GAP = 6
20
+ const TRIGGER_SIZE = s(56)
21
+ const SCREEN_HEIGHT = Dimensions.get('window').height
22
+
23
+ export interface IconPickerProps {
24
+ value: string | null
25
+ onChange: (iconName: string) => void
26
+ label?: string
27
+ error?: string
28
+ hint?: string
29
+ disabled?: boolean
30
+ numColumns?: number
31
+ gap?: number
32
+ style?: ViewStyle
33
+ }
34
+
35
+ function IconCell({ name, selected, size, onPress }: { name: string; selected: boolean; size: number; onPress: () => void }) {
36
+ const { colors } = useTheme()
37
+
38
+ const handlePress = () => {
39
+ hapticSelection()
40
+ onPress()
41
+ }
42
+
43
+ const iconColor = selected ? colors.primaryForeground : colors.foreground
44
+ const bg = selected ? colors.primary : 'transparent'
45
+
46
+ return (
47
+ <TouchableOpacity
48
+ onPress={handlePress}
49
+ activeOpacity={0.6}
50
+ touchSoundDisabled={true}
51
+ accessibilityRole="button"
52
+ accessibilityState={{ selected }}
53
+ accessibilityLabel={name}
54
+ style={[styles.cell, { width: size, height: size, backgroundColor: bg }]}
55
+ >
56
+ {renderIcon(name, ms(20), iconColor)}
57
+ </TouchableOpacity>
58
+ )
59
+ }
60
+
61
+ const IconCellMemo = React.memo(IconCell)
62
+
63
+ export function IconPicker({
64
+ value,
65
+ onChange,
66
+ label,
67
+ error,
68
+ hint,
69
+ disabled = false,
70
+ numColumns = NUM_COLUMNS,
71
+ gap = GAP,
72
+ style,
73
+ }: IconPickerProps) {
74
+ const { colors } = useTheme()
75
+ const insets = useSafeAreaInsets()
76
+ const sheetRef = useRef<BottomSheetModalType>(null)
77
+ const catScrollRef = useRef<any>(null)
78
+ const [open, setOpen] = useState(false)
79
+ const [activeCategory, setActiveCategory] = useState<string | null>(null)
80
+ const [containerWidth, setContainerWidth] = useState(() => Dimensions.get('window').width - s(16) * 2)
81
+
82
+ const sheetName = useId()
83
+
84
+ const activeIcons = useMemo(() => {
85
+ if (activeCategory) {
86
+ return CURATED_ICONS.find((c) => c.name === activeCategory)?.icons ?? ALL_CURATED_ICONS
87
+ }
88
+ return ALL_CURATED_ICONS
89
+ }, [activeCategory])
90
+
91
+ const gapPx = s(gap)
92
+ const cellSize = containerWidth > 0
93
+ ? Math.floor((containerWidth - gapPx * (numColumns - 1)) / numColumns)
94
+ : 0
95
+
96
+ const rows = useMemo(() => {
97
+ const result: string[][] = []
98
+ for (let i = 0; i < activeIcons.length; i += numColumns) {
99
+ result.push(activeIcons.slice(i, i + numColumns))
100
+ }
101
+ return result
102
+ }, [activeIcons, numColumns])
103
+
104
+ useEffect(() => {
105
+ if (open) {
106
+ impactMedium()
107
+ sheetRef.current?.present()
108
+ } else {
109
+ sheetRef.current?.dismiss()
110
+ }
111
+ }, [open])
112
+
113
+ const handleSelect = useCallback(
114
+ (iconName: string) => {
115
+ onChange(iconName)
116
+ setOpen(false)
117
+ setActiveCategory(null)
118
+ },
119
+ [onChange],
120
+ )
121
+
122
+ const handleOpen = useCallback(() => {
123
+ if (disabled) return
124
+ setActiveCategory(null)
125
+ setOpen(true)
126
+ }, [disabled])
127
+
128
+ const handleClose = useCallback(() => {
129
+ setOpen(false)
130
+ setActiveCategory(null)
131
+ }, [])
132
+
133
+ const renderBackdrop = useCallback(
134
+ (props: BottomSheetBackdropProps) => (
135
+ <BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} pressBehavior="close" />
136
+ ),
137
+ [],
138
+ )
139
+
140
+ const selectedIcon = value ? renderIcon(value, ms(28), colors.foreground) : null
141
+
142
+ return (
143
+ <View style={[styles.triggerContainer, style]}>
144
+ {label ? (
145
+ <Text style={[styles.triggerLabel, { color: colors.foreground }]} allowFontScaling={true}>
146
+ {label}
147
+ </Text>
148
+ ) : null}
149
+ <TouchableOpacity
150
+ onPress={handleOpen}
151
+ disabled={disabled}
152
+ activeOpacity={0.7}
153
+ touchSoundDisabled={true}
154
+ accessibilityRole="button"
155
+ accessibilityLabel={label ?? 'Seleccionar icono'}
156
+ accessibilityState={{ disabled }}
157
+ style={[
158
+ styles.trigger,
159
+ {
160
+ backgroundColor: disabled ? colors.surface : colors.background,
161
+ width: TRIGGER_SIZE,
162
+ height: TRIGGER_SIZE,
163
+ borderColor: error ? colors.destructive : value ? colors.primary : colors.border,
164
+ },
165
+ disabled && styles.triggerDisabled,
166
+ ]}
167
+ >
168
+ {selectedIcon ?? renderIcon('plus', ms(24), colors.foregroundMuted)}
169
+ </TouchableOpacity>
170
+ {error ? (
171
+ <Text
172
+ style={[styles.helperText, { color: colors.destructive }]}
173
+ allowFontScaling={true}
174
+ accessibilityLiveRegion="polite"
175
+ >
176
+ {error}
177
+ </Text>
178
+ ) : null}
179
+ {!error && hint ? (
180
+ <Text style={[styles.helperText, { color: colors.foregroundMuted }]} allowFontScaling={true}>
181
+ {hint}
182
+ </Text>
183
+ ) : null}
184
+
185
+ <BottomSheetModal
186
+ ref={sheetRef}
187
+ name={sheetName}
188
+ onDismiss={handleClose}
189
+ enableDynamicSizing={true}
190
+ maxDynamicContentSize={SCREEN_HEIGHT * 0.7}
191
+ backdropComponent={renderBackdrop}
192
+ backgroundStyle={[styles.sheetBackground, { backgroundColor: colors.card }]}
193
+ handleIndicatorStyle={[styles.handle, { backgroundColor: colors.border }]}
194
+ enablePanDownToClose
195
+ topInset={insets.top}
196
+ android_keyboardInputMode="adjustPan"
197
+ >
198
+ <BottomSheetScrollView
199
+ contentContainerStyle={styles.sheetContent}
200
+ showsVerticalScrollIndicator={true}
201
+ >
202
+ {/* Category section label */}
203
+ <Text style={[styles.sectionLabel, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
204
+ Categorías
205
+ </Text>
206
+
207
+ {/* Horizontal scrollable category chips */}
208
+ <ScrollView
209
+ ref={catScrollRef}
210
+ horizontal
211
+ showsHorizontalScrollIndicator={false}
212
+ contentContainerStyle={styles.categoryStrip}
213
+ style={styles.categoryScroll}
214
+ >
215
+ <TouchableOpacity
216
+ onPress={() => setActiveCategory(null)}
217
+ activeOpacity={0.7}
218
+ touchSoundDisabled={true}
219
+ accessibilityRole="button"
220
+ accessibilityLabel="Todos"
221
+ accessibilityState={{ selected: activeCategory === null }}
222
+ style={[
223
+ styles.categoryChip,
224
+ {
225
+ backgroundColor: activeCategory === null ? colors.primary : colors.surface,
226
+ borderColor: activeCategory === null ? colors.primary : colors.border,
227
+ },
228
+ ]}
229
+ >
230
+ <View style={styles.categoryChipInner}>
231
+ {renderIcon('grid', ms(14), activeCategory === null ? colors.primaryForeground : colors.foregroundSubtle)}
232
+ <Text
233
+ style={[
234
+ styles.categoryChipText,
235
+ { color: activeCategory === null ? colors.primaryForeground : colors.foreground },
236
+ ]}
237
+ allowFontScaling={true}
238
+ numberOfLines={1}
239
+ >
240
+ Todos
241
+ </Text>
242
+ </View>
243
+ </TouchableOpacity>
244
+ {CURATED_ICONS.map((cat) => (
245
+ <TouchableOpacity
246
+ key={cat.name}
247
+ onPress={() => setActiveCategory(cat.name)}
248
+ activeOpacity={0.7}
249
+ touchSoundDisabled={true}
250
+ accessibilityRole="button"
251
+ accessibilityLabel={cat.labelEs}
252
+ accessibilityState={{ selected: activeCategory === cat.name }}
253
+ style={[
254
+ styles.categoryChip,
255
+ {
256
+ backgroundColor: activeCategory === cat.name ? colors.primary : colors.surface,
257
+ borderColor: activeCategory === cat.name ? colors.primary : colors.border,
258
+ },
259
+ ]}
260
+ >
261
+ <View style={styles.categoryChipInner}>
262
+ {renderIcon(cat.categoryIcon, ms(14), activeCategory === cat.name ? colors.primaryForeground : colors.foregroundSubtle)}
263
+ <Text
264
+ style={[
265
+ styles.categoryChipText,
266
+ { color: activeCategory === cat.name ? colors.primaryForeground : colors.foreground },
267
+ ]}
268
+ allowFontScaling={true}
269
+ numberOfLines={1}
270
+ >
271
+ {cat.labelEs}
272
+ </Text>
273
+ </View>
274
+ </TouchableOpacity>
275
+ ))}
276
+ </ScrollView>
277
+
278
+ {/* Separator */}
279
+ <View style={[styles.separator, { backgroundColor: colors.border }]} />
280
+
281
+ {/* Icon grid — rendered directly inside BottomSheetScrollView */}
282
+ <View style={styles.gridContainer} onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}>
283
+ {cellSize > 0 ? rows.map((row, i) => (
284
+ <View key={row[0] ?? `row-${i}`} style={[styles.row, { marginBottom: gapPx }]}>
285
+ {row.map((name) => (
286
+ <IconCellMemo
287
+ key={name}
288
+ name={name}
289
+ selected={value === name}
290
+ size={cellSize}
291
+ onPress={() => handleSelect(name)}
292
+ />
293
+ ))}
294
+ {Array.from({ length: numColumns - row.length }).map((_, j) => (
295
+ <View key={`spacer-${j}`} style={{ width: cellSize, height: cellSize }} />
296
+ ))}
297
+ </View>
298
+ )) : null}
299
+ </View>
300
+ </BottomSheetScrollView>
301
+ </BottomSheetModal>
302
+ </View>
303
+ )
304
+ }
305
+
306
+ const styles = StyleSheet.create({
307
+ triggerContainer: {
308
+ gap: vs(8),
309
+ },
310
+ triggerLabel: {
311
+ fontFamily: 'Sohne-Medium',
312
+ fontSize: ms(14),
313
+ },
314
+ trigger: {
315
+ borderRadius: RADIUS.md,
316
+ borderWidth: 1,
317
+ alignItems: 'center',
318
+ justifyContent: 'center',
319
+ },
320
+ triggerDisabled: {
321
+ opacity: 0.6,
322
+ },
323
+ helperText: {
324
+ fontFamily: 'Sohne-Regular',
325
+ fontSize: ms(13),
326
+ },
327
+ sheetBackground: {
328
+ borderTopLeftRadius: ms(16),
329
+ borderTopRightRadius: ms(16),
330
+ },
331
+ handle: {
332
+ width: s(36),
333
+ height: vs(4),
334
+ borderRadius: ms(2),
335
+ },
336
+ sheetContent: {
337
+ paddingHorizontal: s(16),
338
+ paddingBottom: vs(24),
339
+ },
340
+ sectionLabel: {
341
+ fontFamily: 'Sohne-Medium',
342
+ fontSize: ms(12),
343
+ marginBottom: vs(8),
344
+ textTransform: 'uppercase',
345
+ letterSpacing: 0.5,
346
+ },
347
+ categoryScroll: {
348
+ flexGrow: 0,
349
+ flexShrink: 0,
350
+ },
351
+ categoryStrip: {
352
+ gap: s(8),
353
+ },
354
+ categoryChip: {
355
+ borderRadius: RADIUS.full,
356
+ borderWidth: 1,
357
+ paddingVertical: vs(6),
358
+ paddingHorizontal: s(12),
359
+ },
360
+ categoryChipInner: {
361
+ flexDirection: 'row',
362
+ alignItems: 'center',
363
+ gap: s(6),
364
+ },
365
+ categoryChipText: {
366
+ fontFamily: 'Sohne-Medium',
367
+ fontSize: ms(12),
368
+ },
369
+ separator: {
370
+ height: StyleSheet.hairlineWidth,
371
+ marginVertical: vs(12),
372
+ },
373
+ gridContainer: {},
374
+ row: {
375
+ flexDirection: 'row',
376
+ gap: GAP,
377
+ },
378
+ cell: {
379
+ borderRadius: RADIUS.md,
380
+ alignItems: 'center',
381
+ justifyContent: 'center',
382
+ },
383
+ })
@@ -0,0 +1 @@
1
+ export * from './IconPicker'