@retray-dev/ui-kit 12.1.0 → 13.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.
Files changed (282) hide show
  1. package/COMPONENTS.md +183 -147
  2. package/CONSUMER.md +2 -2
  3. package/DESIGN.md +2 -2
  4. package/README.md +13 -8
  5. package/dist/Accordion.d.mts +6 -0
  6. package/dist/Accordion.d.ts +6 -0
  7. package/dist/Accordion.js +62 -208
  8. package/dist/Accordion.mjs +6 -5
  9. package/dist/AlertBanner.js +29 -151
  10. package/dist/AlertBanner.mjs +3 -3
  11. package/dist/AppHeader.js +37 -233
  12. package/dist/AppHeader.mjs +6 -7
  13. package/dist/Avatar.d.mts +17 -1
  14. package/dist/Avatar.d.ts +17 -1
  15. package/dist/Avatar.js +80 -113
  16. package/dist/Avatar.mjs +2 -2
  17. package/dist/Badge.js +24 -147
  18. package/dist/Badge.mjs +3 -3
  19. package/dist/Button.js +86 -274
  20. package/dist/Button.mjs +6 -6
  21. package/dist/Card.js +15 -198
  22. package/dist/Card.mjs +4 -5
  23. package/dist/CategoryStrip.d.mts +0 -5
  24. package/dist/CategoryStrip.d.ts +0 -5
  25. package/dist/CategoryStrip.js +47 -263
  26. package/dist/CategoryStrip.mjs +6 -6
  27. package/dist/Checkbox.js +15 -198
  28. package/dist/Checkbox.mjs +5 -5
  29. package/dist/Chip.js +44 -234
  30. package/dist/Chip.mjs +7 -6
  31. package/dist/ConfirmDialog.js +100 -296
  32. package/dist/ConfirmDialog.mjs +7 -7
  33. package/dist/CurrencyDisplay.js +1 -112
  34. package/dist/CurrencyDisplay.mjs +2 -2
  35. package/dist/CurrencyInput.js +35 -160
  36. package/dist/CurrencyInput.mjs +5 -5
  37. package/dist/DetailRow.js +25 -148
  38. package/dist/DetailRow.mjs +3 -3
  39. package/dist/EmptyState.js +87 -275
  40. package/dist/EmptyState.mjs +7 -7
  41. package/dist/ErrorBoundary.js +32 -197
  42. package/dist/ErrorBoundary.mjs +4 -4
  43. package/dist/Form.js +1 -112
  44. package/dist/Form.mjs +2 -2
  45. package/dist/HolographicCard.d.mts +0 -28
  46. package/dist/HolographicCard.d.ts +0 -28
  47. package/dist/HolographicCard.js +20 -130
  48. package/dist/HolographicCard.mjs +9 -32
  49. package/dist/IconButton.js +36 -232
  50. package/dist/IconButton.mjs +5 -6
  51. package/dist/IconPicker.js +222 -927
  52. package/dist/IconPicker.mjs +5 -5
  53. package/dist/ImageUpload.d.mts +5 -1
  54. package/dist/ImageUpload.d.ts +5 -1
  55. package/dist/ImageUpload.js +32 -215
  56. package/dist/ImageUpload.mjs +5 -6
  57. package/dist/ImageViewer.js +75 -264
  58. package/dist/ImageViewer.mjs +8 -8
  59. package/dist/Input.d.mts +1 -1
  60. package/dist/Input.d.ts +1 -1
  61. package/dist/Input.js +35 -160
  62. package/dist/Input.mjs +4 -4
  63. package/dist/LabelValue.js +24 -147
  64. package/dist/LabelValue.mjs +3 -3
  65. package/dist/ListGroup.js +1 -112
  66. package/dist/ListGroup.mjs +2 -2
  67. package/dist/ListItem.js +38 -233
  68. package/dist/ListItem.mjs +5 -6
  69. package/dist/MediaCard.d.mts +0 -14
  70. package/dist/MediaCard.d.ts +0 -14
  71. package/dist/MediaCard.js +69 -313
  72. package/dist/MediaCard.mjs +5 -6
  73. package/dist/MenuGroup.js +1 -112
  74. package/dist/MenuGroup.mjs +2 -2
  75. package/dist/MenuItem.js +36 -232
  76. package/dist/MenuItem.mjs +5 -6
  77. package/dist/MonthPicker.js +8 -161
  78. package/dist/MonthPicker.mjs +3 -3
  79. package/dist/NumberStepper.js +40 -236
  80. package/dist/NumberStepper.mjs +5 -6
  81. package/dist/PagerDots.d.mts +1 -1
  82. package/dist/PagerDots.d.ts +1 -1
  83. package/dist/PagerDots.js +69 -222
  84. package/dist/PagerDots.mjs +6 -5
  85. package/dist/Pressable.js +14 -85
  86. package/dist/Pressable.mjs +4 -4
  87. package/dist/PricingCard.js +94 -279
  88. package/dist/PricingCard.mjs +8 -8
  89. package/dist/Progress.js +3 -121
  90. package/dist/Progress.mjs +3 -3
  91. package/dist/RadioGroup.js +52 -263
  92. package/dist/RadioGroup.mjs +5 -5
  93. package/dist/RetrayProvider.d.mts +1 -1
  94. package/dist/RetrayProvider.d.ts +1 -1
  95. package/dist/RetrayProvider.js +5 -6
  96. package/dist/RetrayProvider.mjs +3 -3
  97. package/dist/Select.d.mts +2 -1
  98. package/dist/Select.d.ts +2 -1
  99. package/dist/Select.js +24 -230
  100. package/dist/Select.mjs +4 -5
  101. package/dist/SelectableCard.d.mts +27 -0
  102. package/dist/SelectableCard.d.ts +27 -0
  103. package/dist/SelectableCard.js +335 -0
  104. package/dist/SelectableCard.mjs +8 -0
  105. package/dist/SelectableGrid.d.mts +0 -21
  106. package/dist/SelectableGrid.d.ts +0 -21
  107. package/dist/SelectableGrid.js +49 -269
  108. package/dist/SelectableGrid.mjs +5 -6
  109. package/dist/Separator.js +1 -112
  110. package/dist/Separator.mjs +2 -2
  111. package/dist/Sheet.js +16 -163
  112. package/dist/Sheet.mjs +3 -3
  113. package/dist/SheetSelect.js +39 -234
  114. package/dist/SheetSelect.mjs +6 -6
  115. package/dist/Skeleton.d.mts +3 -1
  116. package/dist/Skeleton.d.ts +3 -1
  117. package/dist/Skeleton.js +7 -124
  118. package/dist/Skeleton.mjs +3 -3
  119. package/dist/Slider.js +6 -159
  120. package/dist/Slider.mjs +3 -3
  121. package/dist/Spinner.js +3 -114
  122. package/dist/Spinner.mjs +2 -2
  123. package/dist/Stats.d.mts +4 -1
  124. package/dist/Stats.d.ts +4 -1
  125. package/dist/Stats.js +60 -234
  126. package/dist/Stats.mjs +5 -6
  127. package/dist/Switch.js +24 -173
  128. package/dist/Switch.mjs +5 -4
  129. package/dist/TabBar.js +43 -198
  130. package/dist/TabBar.mjs +5 -4
  131. package/dist/Tabs.js +15 -197
  132. package/dist/Tabs.mjs +5 -5
  133. package/dist/Text.js +9 -128
  134. package/dist/Text.mjs +2 -2
  135. package/dist/Textarea.d.mts +2 -1
  136. package/dist/Textarea.d.ts +2 -1
  137. package/dist/Textarea.js +71 -217
  138. package/dist/Textarea.mjs +4 -4
  139. package/dist/Toast.js +1 -112
  140. package/dist/Toast.mjs +2 -2
  141. package/dist/Toggle.js +39 -234
  142. package/dist/Toggle.mjs +6 -6
  143. package/dist/{chunk-FFTYLPSB.mjs → chunk-2QOHHBJC.mjs} +13 -7
  144. package/dist/{chunk-BCWEHE34.mjs → chunk-2VIDP72N.mjs} +3 -3
  145. package/dist/{chunk-PGERH3P7.mjs → chunk-4NQFTHN3.mjs} +13 -7
  146. package/dist/{chunk-3N2M3WZL.mjs → chunk-4ZO5PTKF.mjs} +4 -4
  147. package/dist/{chunk-MYZ2EDYU.mjs → chunk-5MYNAAFE.mjs} +13 -17
  148. package/dist/{chunk-E7NEHHXV.mjs → chunk-62BBSSUF.mjs} +3 -3
  149. package/dist/{chunk-ISY26JQJ.mjs → chunk-6CR4S6W2.mjs} +3 -3
  150. package/dist/{chunk-FUVYSVGR.mjs → chunk-6QLBHUEG.mjs} +8 -7
  151. package/dist/chunk-ARONDO7M.mjs +40 -0
  152. package/dist/{chunk-3UYAZ7I4.mjs → chunk-AZV7KNJI.mjs} +3 -3
  153. package/dist/{chunk-HLMPMUK2.mjs → chunk-BTUW5LSG.mjs} +11 -8
  154. package/dist/chunk-BULKGOIZ.mjs +235 -0
  155. package/dist/{chunk-265G6A46.mjs → chunk-CBIZLRYH.mjs} +29 -12
  156. package/dist/chunk-CM2DG4MR.mjs +142 -0
  157. package/dist/{chunk-2I2AYECM.mjs → chunk-DBHSUUKU.mjs} +2 -2
  158. package/dist/{chunk-P64WHW4A.mjs → chunk-DE25XTVQ.mjs} +3 -3
  159. package/dist/{chunk-DI7CBDL6.mjs → chunk-E4EQSCKR.mjs} +5 -5
  160. package/dist/{chunk-357YO24D.mjs → chunk-EHGBHFMH.mjs} +9 -17
  161. package/dist/{chunk-GK4VRMNE.mjs → chunk-EROPDCB5.mjs} +24 -27
  162. package/dist/{chunk-XBAGGKLW.mjs → chunk-ERWJPVX7.mjs} +2 -2
  163. package/dist/{chunk-LRM4AVYY.mjs → chunk-ESQDPO5E.mjs} +7 -7
  164. package/dist/{chunk-EFLFRAHD.mjs → chunk-EW2FIDSM.mjs} +1 -1
  165. package/dist/{chunk-7HSILTC4.mjs → chunk-FTTI6T5Q.mjs} +4 -4
  166. package/dist/{chunk-X26S5EVZ.mjs → chunk-HUSSF6TF.mjs} +1 -1
  167. package/dist/chunk-IFYMBOEN.mjs +14 -0
  168. package/dist/{chunk-S3KJCPEJ.mjs → chunk-IGU223UM.mjs} +80 -4
  169. package/dist/chunk-IJCMPVW5.mjs +121 -0
  170. package/dist/{chunk-I4V5XZPS.mjs → chunk-ITG4JQM3.mjs} +4 -4
  171. package/dist/{chunk-F4V6XLP4.mjs → chunk-K3QX2M26.mjs} +11 -8
  172. package/dist/{chunk-V6NFJXKO.mjs → chunk-K7TKID3V.mjs} +8 -7
  173. package/dist/{chunk-ZHMSAYLT.mjs → chunk-KAGADD2O.mjs} +4 -4
  174. package/dist/{chunk-3GEYJ7I5.mjs → chunk-KC5QDYGZ.mjs} +4 -4
  175. package/dist/{chunk-HJ46DTJE.mjs → chunk-KPTY7UYQ.mjs} +1 -1
  176. package/dist/{chunk-EMUWGDWC.mjs → chunk-KSSVIFYR.mjs} +11 -12
  177. package/dist/chunk-L3YKPTJQ.mjs +119 -0
  178. package/dist/chunk-M53LC4Q7.mjs +35 -0
  179. package/dist/{chunk-NXI4YDZ2.mjs → chunk-MP7GLMIR.mjs} +17 -25
  180. package/dist/chunk-MZ6WRTD2.mjs +40 -0
  181. package/dist/chunk-NGEN2EES.mjs +581 -0
  182. package/dist/{chunk-JULSIZDM.mjs → chunk-OBV72JD4.mjs} +1 -1
  183. package/dist/{chunk-2A2LEFZG.mjs → chunk-PGQ6FMXS.mjs} +6 -5
  184. package/dist/{chunk-BQZE3HAW.mjs → chunk-PI6RULJX.mjs} +1 -1
  185. package/dist/{chunk-FA2KMTH5.mjs → chunk-RA6SAAFE.mjs} +9 -8
  186. package/dist/{chunk-FVTVCJAH.mjs → chunk-RRKM4MKB.mjs} +7 -7
  187. package/dist/{chunk-AKM4EPOT.mjs → chunk-S2VGME7X.mjs} +1 -1
  188. package/dist/{chunk-OULVKTWL.mjs → chunk-S44XWTTC.mjs} +35 -25
  189. package/dist/{chunk-QSFV2P7O.mjs → chunk-SZEKQAOY.mjs} +1 -1
  190. package/dist/{chunk-N4ZPVCJH.mjs → chunk-TETMEKZE.mjs} +9 -9
  191. package/dist/{chunk-2CBQKU7H.mjs → chunk-TMH263OK.mjs} +5 -4
  192. package/dist/{chunk-D3Y2T42P.mjs → chunk-U6DEBYU5.mjs} +10 -9
  193. package/dist/{chunk-4WFMPFZB.mjs → chunk-UOKFSFNJ.mjs} +2 -2
  194. package/dist/{chunk-WOEWGSTU.mjs → chunk-URIH43IJ.mjs} +13 -21
  195. package/dist/{chunk-JCZQOY4O.mjs → chunk-V2ZB2XNS.mjs} +16 -10
  196. package/dist/{chunk-P73V2EKS.mjs → chunk-WIPEDNSD.mjs} +7 -7
  197. package/dist/{chunk-BOVUP27T.mjs → chunk-XCIG6HT2.mjs} +6 -5
  198. package/dist/chunk-Y6YS33GM.mjs +131 -0
  199. package/dist/{chunk-5OLNXP3S.mjs → chunk-ZKDKKQCE.mjs} +29 -7
  200. package/dist/{chunk-DF6DU42P.mjs → chunk-ZTPYUU5C.mjs} +5 -5
  201. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  202. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  203. package/dist/index.d.mts +15 -74
  204. package/dist/index.d.ts +15 -74
  205. package/dist/index.js +1055 -1562
  206. package/dist/index.mjs +81 -84
  207. package/package.json +8 -10
  208. package/src/components/Accordion/Accordion.tsx +32 -9
  209. package/src/components/AlertBanner/AlertBanner.tsx +7 -6
  210. package/src/components/AppHeader/AppHeader.tsx +1 -1
  211. package/src/components/Avatar/Avatar.tsx +92 -1
  212. package/src/components/Avatar/index.ts +2 -2
  213. package/src/components/Badge/Badge.tsx +2 -2
  214. package/src/components/Button/Button.tsx +64 -57
  215. package/src/components/Card/Card.tsx +1 -0
  216. package/src/components/CategoryStrip/CategoryStrip.tsx +36 -49
  217. package/src/components/Chip/Chip.tsx +5 -4
  218. package/src/components/ConfirmDialog/ConfirmDialog.tsx +13 -6
  219. package/src/components/DetailRow/DetailRow.tsx +3 -3
  220. package/src/components/EmptyState/EmptyState.tsx +2 -2
  221. package/src/components/ErrorBoundary/ErrorBoundary.tsx +6 -6
  222. package/src/components/HolographicCard/HolographicCard.tsx +14 -95
  223. package/src/components/IconButton/IconButton.tsx +2 -2
  224. package/src/components/IconPicker/IconPicker.tsx +13 -12
  225. package/src/components/ImageUpload/ImageUpload.tsx +24 -28
  226. package/src/components/ImageViewer/ImageViewer.tsx +3 -3
  227. package/src/components/Input/Input.tsx +11 -5
  228. package/src/components/LabelValue/LabelValue.tsx +2 -2
  229. package/src/components/ListItem/ListItem.tsx +4 -4
  230. package/src/components/MediaCard/MediaCard.tsx +21 -59
  231. package/src/components/MenuItem/MenuItem.tsx +2 -2
  232. package/src/components/MonthPicker/MonthPicker.tsx +2 -2
  233. package/src/components/NumberStepper/NumberStepper.tsx +6 -6
  234. package/src/components/PagerDots/PagerDots.tsx +38 -28
  235. package/src/components/PricingCard/PricingCard.tsx +6 -6
  236. package/src/components/RadioGroup/RadioGroup.tsx +18 -31
  237. package/src/components/Select/Select.tsx +32 -39
  238. package/src/components/SelectableCard/SelectableCard.tsx +302 -0
  239. package/src/components/SelectableCard/index.ts +1 -0
  240. package/src/components/SelectableGrid/SelectableGrid.tsx +38 -72
  241. package/src/components/Sheet/Sheet.tsx +11 -4
  242. package/src/components/SheetSelect/SheetSelect.tsx +3 -3
  243. package/src/components/Skeleton/Skeleton.tsx +6 -3
  244. package/src/components/Spinner/Spinner.tsx +2 -2
  245. package/src/components/Stats/Stats.tsx +36 -8
  246. package/src/components/Switch/Switch.tsx +9 -6
  247. package/src/components/TabBar/TabBar.tsx +9 -8
  248. package/src/components/Text/Text.tsx +12 -1
  249. package/src/components/Textarea/Textarea.tsx +18 -32
  250. package/src/components/Toggle/Toggle.tsx +3 -3
  251. package/src/hooks/useConfirmDialog.ts +31 -42
  252. package/src/index.ts +4 -4
  253. package/src/theme/ThemeProvider.tsx +1 -4
  254. package/src/theme/colorUtils.ts +1 -72
  255. package/src/theme/colors.ts +47 -1
  256. package/src/theme/types.ts +6 -3
  257. package/src/utils/animations.ts +0 -47
  258. package/src/utils/curatedIcons.ts +93 -801
  259. package/src/utils/haptics.ts +13 -208
  260. package/src/utils/icons.ts +27 -91
  261. package/src/utils/pressable.ts +10 -61
  262. package/dist/VirtualList.d.mts +0 -19
  263. package/dist/VirtualList.d.ts +0 -19
  264. package/dist/VirtualList.js +0 -38
  265. package/dist/VirtualList.mjs +0 -2
  266. package/dist/chunk-3DKJ2GIC.mjs +0 -30
  267. package/dist/chunk-AQEVCEXV.mjs +0 -164
  268. package/dist/chunk-DOGIPOF5.mjs +0 -131
  269. package/dist/chunk-DVK4G2GT.mjs +0 -59
  270. package/dist/chunk-EJ7ZPXOH.mjs +0 -163
  271. package/dist/chunk-J6Q2YJEV.mjs +0 -134
  272. package/dist/chunk-JNVAIDLK.mjs +0 -136
  273. package/dist/chunk-KA7LTET3.mjs +0 -71
  274. package/dist/chunk-KHYX4IOM.mjs +0 -1114
  275. package/dist/chunk-NC5ZTR2Y.mjs +0 -32
  276. package/dist/chunk-YNROWHQJ.mjs +0 -46
  277. package/src/components/VirtualList/VirtualList.tsx +0 -60
  278. package/src/components/VirtualList/index.ts +0 -1
  279. package/src/utils/fontGuard.ts +0 -35
  280. package/src/utils/hover.ts +0 -25
  281. package/src/utils/useColorTransition.ts +0 -40
  282. package/src/utils/usePressScale.ts +0 -75
@@ -4,10 +4,11 @@ import { impactLight } from '../../utils/haptics'
4
4
  import { useTheme } from '../../theme'
5
5
  import { s, vs, ms, mvs } from '../../utils/scaling'
6
6
  import { RADIUS } from '../../tokens'
7
- import { renderIcon } from '../../utils/icons'
7
+ import { Icon } from '../../utils/icons'
8
8
  import { PressableCard } from '../../utils/pressable'
9
9
 
10
10
  export type StatsVariant = 'elevated' | 'outlined' | 'filled'
11
+ export type StatsSize = 'default' | 'compact'
11
12
 
12
13
  export interface StatsProps {
13
14
  value: string
@@ -17,6 +18,8 @@ export interface StatsProps {
17
18
  iconName?: string
18
19
  iconColor?: string
19
20
  variant?: StatsVariant
21
+ /** `'compact'` reduces everything proportionally for tight grids. */
22
+ size?: StatsSize
20
23
  onPress?: () => void
21
24
  style?: ViewStyle
22
25
  accessibilityLabel?: string
@@ -38,6 +41,7 @@ function StatsComponent({
38
41
  iconName,
39
42
  iconColor,
40
43
  variant = 'elevated',
44
+ size = 'default',
41
45
  onPress,
42
46
  style,
43
47
  accessibilityLabel,
@@ -60,6 +64,30 @@ function StatsComponent({
60
64
 
61
65
  const isCompact = containerWidth > 0 && containerWidth < COMPACT_THRESHOLD && !!(icon ?? iconName)
62
66
 
67
+ const sizeStyles = size === 'compact'
68
+ ? {
69
+ valueFontFamily: 'Sohne-SemiBold' as const,
70
+ valueFontSize: ms(16),
71
+ valueLineHeight: mvs(20),
72
+ labelFontSize: ms(11),
73
+ labelLineHeight: mvs(14),
74
+ descriptionFontSize: ms(10),
75
+ descriptionLineHeight: mvs(14),
76
+ iconSize: ms(18),
77
+ padding: s(12),
78
+ }
79
+ : {
80
+ valueFontFamily: 'Sohne-Bold' as const,
81
+ valueFontSize: ms(21),
82
+ valueLineHeight: mvs(25),
83
+ labelFontSize: ms(13),
84
+ labelLineHeight: mvs(18),
85
+ descriptionFontSize: ms(12),
86
+ descriptionLineHeight: mvs(16),
87
+ iconSize: ms(20),
88
+ padding: s(16),
89
+ }
90
+
63
91
  const variantStyle: ViewStyle = {
64
92
  elevated: {
65
93
  backgroundColor: colors.card,
@@ -86,29 +114,29 @@ function StatsComponent({
86
114
 
87
115
  const iconColorResolved = iconColor ?? colors.primary
88
116
 
89
- const resolvedIcon = iconName ? renderIcon(iconName, ms(22), iconColorResolved) : icon
117
+ const resolvedIcon = iconName ? <Icon name={iconName} size={sizeStyles.iconSize} color={iconColorResolved} /> : icon
90
118
 
91
119
  const iconElement = resolvedIcon ? (
92
120
  <View style={styles.iconWrapper}>{resolvedIcon}</View>
93
121
  ) : null
94
122
 
95
123
  const valueElement = (
96
- <Text style={[styles.value, { color: colors.foreground }]} allowFontScaling={true}>
124
+ <Text style={[styles.value, { color: colors.foreground, fontFamily: sizeStyles.valueFontFamily, fontSize: sizeStyles.valueFontSize, lineHeight: sizeStyles.valueLineHeight }]} allowFontScaling={true}>
97
125
  {value}
98
126
  </Text>
99
127
  )
100
128
 
101
129
  const cardContent = (
102
- <View style={[styles.card, variantStyle, style]} onLayout={handleLayout}>
130
+ <View style={[styles.card, variantStyle, { padding: sizeStyles.padding }, style]} onLayout={handleLayout}>
103
131
  {isCompact ? (
104
132
  <>
105
133
  {iconElement}
106
134
  <View style={styles.compactValue}>{valueElement}</View>
107
- <Text style={[styles.label, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
135
+ <Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
108
136
  {label}
109
137
  </Text>
110
138
  {description ? (
111
- <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
139
+ <Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
112
140
  {description}
113
141
  </Text>
114
142
  ) : null}
@@ -119,11 +147,11 @@ function StatsComponent({
119
147
  {iconElement}
120
148
  {valueElement}
121
149
  </View>
122
- <Text style={[styles.label, { color: colors.foregroundSubtle }]} allowFontScaling={true}>
150
+ <Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
123
151
  {label}
124
152
  </Text>
125
153
  {description ? (
126
- <Text style={[styles.description, { color: colors.foregroundMuted }]} allowFontScaling={true}>
154
+ <Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
127
155
  {description}
128
156
  </Text>
129
157
  ) : null}
@@ -1,11 +1,12 @@
1
1
  import React from 'react'
2
- import { TouchableOpacity, StyleSheet, ViewStyle, View } from 'react-native'
2
+ import { StyleSheet, ViewStyle, View } from 'react-native'
3
3
  import { EaseView } from 'react-native-ease'
4
4
  import { Feather } from '@expo/vector-icons'
5
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
6
6
  import { useTheme } from '../../theme'
7
7
  import { s } from '../../utils/scaling'
8
8
  import { COLOR_TRANSITION, OPACITY_TRANSITION, SPRING_ELASTIC } from '../../utils/animations'
9
+ import { PressableButton } from '../../utils/pressable'
9
10
 
10
11
  const TRACK_WIDTH = s(52)
11
12
  const TRACK_HEIGHT = s(30)
@@ -30,14 +31,14 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
30
31
 
31
32
  return (
32
33
  <View style={[{ alignSelf: 'flex-start' }, style]}>
33
- <TouchableOpacity
34
+ <PressableButton
34
35
  onPress={() => {
35
36
  hapticSelection()
36
37
  onCheckedChange?.(!checked)
37
38
  }}
38
- disabled={disabled}
39
- activeOpacity={0.8}
40
- touchSoundDisabled={true}
39
+ enabled={!disabled}
40
+ rippleColor="transparent"
41
+ touchSoundDisabled
41
42
  accessibilityRole="switch"
42
43
  accessibilityLabel={accessibilityLabel}
43
44
  accessibilityState={{ checked, disabled: isDisabled }}
@@ -76,7 +77,7 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
76
77
  </EaseView>
77
78
  </EaseView>
78
79
  </View>
79
- </TouchableOpacity>
80
+ </PressableButton>
80
81
  </View>
81
82
  )
82
83
  }
@@ -84,6 +85,8 @@ export function Switch({ checked = false, onCheckedChange, disabled, style, acce
84
85
  const styles = StyleSheet.create({
85
86
  touchable: {
86
87
  alignSelf: 'flex-start',
88
+ minHeight: 44,
89
+ justifyContent: 'center',
87
90
  },
88
91
  trackContainer: {
89
92
  position: 'relative',
@@ -1,10 +1,11 @@
1
1
  import React from 'react'
2
- import { View, Text, TouchableOpacity, StyleSheet, ViewStyle } from 'react-native'
2
+ import { View, Text, StyleSheet, ViewStyle } from 'react-native'
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
4
4
  import { useTheme } from '../../theme'
5
- import { renderIcon } from '../../utils/icons'
5
+ import { Icon } from '../../utils/icons'
6
6
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
7
7
  import { s, vs, ms, mvs } from '../../utils/scaling'
8
+ import { PressableTab } from '../../utils/pressable'
8
9
 
9
10
  export interface TabBarItem {
10
11
  /** Unique key for the tab. */
@@ -74,23 +75,23 @@ export function TabBar({
74
75
  {items.map((item) => {
75
76
  const active = item.key === activeKey
76
77
  const tint = active ? resolvedActive : resolvedInactive
77
- const iconNode = item.icon ?? (item.iconName ? renderIcon(item.iconName, ms(24), tint) : null)
78
+ const iconNode = item.icon ?? (item.iconName ? <Icon name={item.iconName} size={ms(24)} color={tint} /> : null)
78
79
  const showBadge = item.badge !== undefined && item.badge !== false
79
80
  const badgeCount = typeof item.badge === 'number' ? item.badge : undefined
80
81
 
81
82
  return (
82
- <TouchableOpacity
83
+ <PressableTab
83
84
  key={item.key}
84
- style={styles.tab}
85
85
  onPress={() => {
86
86
  if (!active) hapticSelection()
87
87
  onTabPress(item.key)
88
88
  }}
89
- activeOpacity={0.7}
90
- touchSoundDisabled={true}
89
+ rippleColor="transparent"
90
+ touchSoundDisabled
91
91
  accessibilityRole="tab"
92
92
  accessibilityState={{ selected: active }}
93
93
  accessibilityLabel={item.label ?? item.key}
94
+ style={styles.tab}
94
95
  >
95
96
  <View>
96
97
  {iconNode}
@@ -115,7 +116,7 @@ export function TabBar({
115
116
  {item.label}
116
117
  </Text>
117
118
  ) : null}
118
- </TouchableOpacity>
119
+ </PressableTab>
119
120
  )
120
121
  })}
121
122
  </View>
@@ -1,9 +1,11 @@
1
1
  import React from 'react'
2
2
  import { Text as RNText, TextProps as RNTextProps, TextStyle } from 'react-native'
3
+
4
+ declare const __DEV__: boolean | undefined
3
5
  import { useTheme } from '../../theme'
4
6
  import { TYPOGRAPHY } from '../../tokens'
5
7
  import { ms, mvs } from '../../utils/scaling'
6
- import { warnIfFontsMissing } from '../../utils/fontGuard'
8
+ import { isLoaded as expoFontIsLoaded } from 'expo-font'
7
9
 
8
10
  export type TextVariant =
9
11
  | 'display-hero'
@@ -70,6 +72,15 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
70
72
  'button-sm': 'foreground',
71
73
  }
72
74
 
75
+ let fontWarned = false
76
+ function warnIfFontsMissing(): void {
77
+ if (fontWarned || typeof __DEV__ === 'undefined' || !__DEV__) return
78
+ fontWarned = true
79
+ if (!expoFontIsLoaded('Sohne-Regular')) {
80
+ console.warn('[retray-ui-kit] Sohne fonts not loaded — text falls back to system font.')
81
+ }
82
+ }
83
+
73
84
  function TextBase({ variant = 'body-md', color, style, uppercase, children, ...props }: TextProps) {
74
85
  warnIfFontsMissing()
75
86
  const { colors } = useTheme()
@@ -1,15 +1,10 @@
1
1
  import React, { useState } from 'react'
2
2
  import { TextInput, View, Text, StyleSheet, TextInputProps, ViewStyle, Platform } from 'react-native'
3
- import Animated, {
4
- useAnimatedStyle,
5
- interpolateColor,
6
- interpolate,
7
- } from 'react-native-reanimated'
3
+ import { EaseView } from 'react-native-ease'
8
4
  import { useTheme } from '../../theme'
9
5
  import { s, vs, ms } from '../../utils/scaling'
10
- import { renderIcon } from '../../utils/icons'
11
- import { useColorTransition } from '../../utils/useColorTransition'
12
- import { TIMINGS } from '../../utils/animations'
6
+ import { Icon } from '../../utils/icons'
7
+ import { COLOR_TRANSITION } from '../../utils/animations'
13
8
 
14
9
  const webInputResetStyle: Record<string, unknown> =
15
10
  Platform.OS === 'web'
@@ -20,6 +15,7 @@ export interface TextareaProps extends TextInputProps {
20
15
  label?: string
21
16
  error?: string
22
17
  hint?: string
18
+ disabled?: boolean
23
19
  rows?: number
24
20
  prefixIcon?: string
25
21
  prefixIconNode?: React.ReactNode
@@ -31,6 +27,7 @@ export function Textarea({
31
27
  label,
32
28
  error,
33
29
  hint,
30
+ disabled,
34
31
  rows = 4,
35
32
  prefixIcon,
36
33
  prefixIconNode,
@@ -44,46 +41,36 @@ export function Textarea({
44
41
  }: TextareaProps) {
45
42
  const { colors } = useTheme()
46
43
  const [focused, setFocused] = useState(false)
47
- const focusProgress = useColorTransition(focused, {
48
- duration: focused ? TIMINGS.focusIn.duration : TIMINGS.focusOut.duration,
49
- })
50
44
 
51
45
  const resolvedPrefixIcon = prefixIcon
52
- ? renderIcon(prefixIcon, ms(16), prefixIconColor ?? colors.foregroundMuted)
46
+ ? <Icon name={prefixIcon} size={ms(16)} color={prefixIconColor ?? colors.foregroundMuted} />
53
47
  : prefixIconNode
54
48
 
55
- // Border drawn on an absolute overlay (mirrors Input.tsx) so the 1px→2px
56
- // focus weight change never resizes the box / reflows content.
57
- const borderAnimStyle = useAnimatedStyle(() => ({
58
- borderColor: error
59
- ? colors.destructive
60
- : interpolateColor(focusProgress.value, [0, 1], [colors.border, colors.primary]),
61
- borderWidth: error
62
- ? 2
63
- : interpolate(focusProgress.value, [0, 1], [1, 2]),
64
- }))
49
+ const borderColor = error ? colors.destructive : (focused ? colors.primary : colors.border)
65
50
 
66
51
  return (
67
52
  <View style={[styles.container, containerStyle]}>
68
53
  {label ? <Text style={[styles.label, { color: colors.foreground }]} allowFontScaling={true}>{label}</Text> : null}
69
- <Animated.View
70
- style={[
71
- styles.inputWrapper,
72
- { backgroundColor: colors.background },
73
- ]}
74
- >
75
- <Animated.View style={[styles.borderOverlay, borderAnimStyle]} pointerEvents="none" />
54
+ <View style={[styles.inputWrapper, { backgroundColor: colors.background }]}>
55
+ <EaseView
56
+ style={[styles.borderOverlay, { borderWidth: error ? 2 : 1 }]}
57
+ animate={{ borderColor }}
58
+ transition={COLOR_TRANSITION}
59
+ pointerEvents="none"
60
+ />
76
61
  {resolvedPrefixIcon ? <View style={styles.prefixIcon}>{resolvedPrefixIcon}</View> : null}
77
62
  <TextInput
78
63
  multiline
79
64
  numberOfLines={rows}
80
65
  textAlignVertical="top"
66
+ editable={!disabled}
81
67
  style={[
82
68
  styles.input,
83
69
  {
84
70
  color: colors.foreground,
85
71
  minHeight: rows * vs(30),
86
72
  },
73
+ disabled && { opacity: 0.45 },
87
74
  webInputResetStyle,
88
75
  style,
89
76
  ]}
@@ -98,9 +85,10 @@ export function Textarea({
98
85
  placeholderTextColor={colors.foregroundMuted}
99
86
  allowFontScaling={true}
100
87
  accessibilityLabel={accessibilityLabel ?? label}
88
+ accessibilityState={{ disabled: !!disabled }}
101
89
  {...props}
102
90
  />
103
- </Animated.View>
91
+ </View>
104
92
  {error ? (
105
93
  <Text
106
94
  style={[styles.helperText, { color: colors.destructive }]}
@@ -128,8 +116,6 @@ const styles = StyleSheet.create({
128
116
  marginBottom: vs(2),
129
117
  },
130
118
  inputWrapper: {
131
- // Border lives on borderOverlay (absolute); wrapper carries none so the
132
- // focus weight change never reflows content.
133
119
  borderRadius: 8,
134
120
  paddingHorizontal: s(14),
135
121
  paddingVertical: vs(11),
@@ -5,7 +5,7 @@ import { FontAwesome5 } from '@expo/vector-icons'
5
5
  import { selectionAsync as hapticSelection } from '../../utils/haptics'
6
6
  import { useTheme } from '../../theme'
7
7
  import { s, vs, ms } from '../../utils/scaling'
8
- import { renderIcon } from '../../utils/icons'
8
+ import { Icon } from '../../utils/icons'
9
9
  import { COLOR_TRANSITION } from '../../utils/animations'
10
10
  import { PressableButton } from '../../utils/pressable'
11
11
 
@@ -30,13 +30,13 @@ function ToggleIcon({ pressed, iconName, activeIconName, icon, activeIcon, iconC
30
30
  }
31
31
 
32
32
  if (pressed) {
33
- if (activeIconName) return <>{renderIcon(activeIconName, iconSize, activeIconColor ?? primaryColor)}</>
33
+ if (activeIconName) return <Icon name={activeIconName} size={iconSize} color={activeIconColor ?? primaryColor} />
34
34
  const active = renderProp(activeIcon)
35
35
  if (active) return <>{active}</>
36
36
  return <FontAwesome5 name="check-circle" size={iconSize} color={primaryColor} />
37
37
  }
38
38
 
39
- if (iconName) return <>{renderIcon(iconName, iconSize, iconColor ?? mutedColor)}</>
39
+ if (iconName) return <Icon name={iconName} size={iconSize} color={iconColor ?? mutedColor} />
40
40
  const custom = renderProp(icon)
41
41
  if (custom) return <>{custom}</>
42
42
 
@@ -1,67 +1,56 @@
1
- import { useState, useCallback } from 'react'
1
+ import { useState, useCallback, useRef, useEffect } from 'react'
2
2
 
3
3
  export interface UseConfirmDialogOptions {
4
4
  onConfirm: () => void | Promise<void>
5
5
  onCancel?: () => void
6
6
  }
7
7
 
8
- export interface UseConfirmDialogResult<T> {
9
- /** Pass to ConfirmDialog `visible` prop. */
8
+ export interface UseConfirmDialogResult {
10
9
  visible: boolean
11
- /** The value passed to `open()` — available during the confirmation flow. */
12
- target: T | null
13
- /** Whether `onConfirm` is currently executing. Pass to ConfirmDialog `loading` prop. */
14
10
  loading: boolean
15
- /** Open the dialog, optionally with an associated value (e.g. the item to delete). */
16
- open: (target?: T) => void
17
- /** Handlers to pass directly to ConfirmDialog. */
18
- dialogProps: {
19
- visible: boolean
20
- loading: boolean
21
- onConfirm: () => void
22
- onCancel: () => void
23
- }
11
+ open: () => void
12
+ onConfirm: () => void
13
+ onCancel: () => void
24
14
  }
25
15
 
26
- export function useConfirmDialog<T = undefined>(
27
- options: UseConfirmDialogOptions,
28
- ): UseConfirmDialogResult<T> {
16
+ export function useConfirmDialog(options: UseConfirmDialogOptions): UseConfirmDialogResult {
29
17
  const [visible, setVisible] = useState(false)
30
- const [target, setTarget] = useState<T | null>(null)
31
18
  const [loading, setLoading] = useState(false)
19
+ const mountedRef = useRef(true)
20
+ const onConfirmRef = useRef(options.onConfirm)
21
+ const onCancelRef = useRef(options.onCancel)
32
22
 
33
- const open = useCallback((t?: T) => {
34
- setTarget(t ?? null)
35
- setVisible(true)
23
+ useEffect(() => {
24
+ onConfirmRef.current = options.onConfirm
25
+ onCancelRef.current = options.onCancel
26
+ })
27
+
28
+ useEffect(() => {
29
+ return () => {
30
+ mountedRef.current = false
31
+ }
36
32
  }, [])
37
33
 
34
+ const open = useCallback(() => setVisible(true), [])
35
+
38
36
  const handleConfirm = useCallback(async () => {
39
37
  setLoading(true)
40
38
  try {
41
- await options.onConfirm()
39
+ await onConfirmRef.current()
40
+ } catch {
41
+ /* consumer handles error in onConfirm */
42
42
  } finally {
43
- setLoading(false)
44
- setVisible(false)
45
- setTarget(null)
43
+ if (mountedRef.current) {
44
+ setLoading(false)
45
+ setVisible(false)
46
+ }
46
47
  }
47
- }, [options])
48
+ }, [])
48
49
 
49
50
  const handleCancel = useCallback(() => {
50
51
  setVisible(false)
51
- setTarget(null)
52
- options.onCancel?.()
53
- }, [options])
52
+ onCancelRef.current?.()
53
+ }, [])
54
54
 
55
- return {
56
- visible,
57
- target,
58
- loading,
59
- open,
60
- dialogProps: {
61
- visible,
62
- loading,
63
- onConfirm: handleConfirm,
64
- onCancel: handleCancel,
65
- },
66
- }
55
+ return { visible, loading, open, onConfirm: handleConfirm, onCancel: handleCancel }
67
56
  }
package/src/index.ts CHANGED
@@ -44,12 +44,12 @@ export * from './components/CategoryStrip'
44
44
  export * from './components/Pressable'
45
45
  export * from './components/DetailRow'
46
46
  export * from './components/Form'
47
- export * from './components/VirtualList'
48
47
  export * from './components/RetrayProvider'
49
48
  export * from './components/ErrorBoundary'
50
49
  export * from './components/PagerDots'
51
50
  export * from './components/AppHeader'
52
51
  export * from './components/SelectableGrid'
52
+ export * from './components/SelectableCard'
53
53
  export * from './components/PricingCard'
54
54
  export * from './components/TabBar'
55
55
  export * from './components/ImageViewer'
@@ -63,10 +63,10 @@ export * from './components/Stats'
63
63
  // barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
64
64
 
65
65
  // Icon utility
66
- export { Icon, renderIcon, configureIconFamilies, getValidIconNames } from './utils/icons'
66
+ export { Icon } from './utils/icons'
67
67
 
68
68
  // Color utilities
69
- export { withAlpha } from './theme/colorUtils'
69
+ export { withAlpha, hexToRgb } from './theme/colorUtils'
70
70
 
71
71
  // Typography utilities
72
72
  export { getResponsiveFontSize } from './utils/typography'
@@ -81,13 +81,13 @@ export {
81
81
  notificationSuccess,
82
82
  notificationError,
83
83
  notificationWarning,
84
- richHaptics,
85
84
  } from './utils/haptics'
86
85
 
87
86
  // Hooks
88
87
  export { useConfirmDialog } from './hooks/useConfirmDialog'
89
88
  export type { UseConfirmDialogOptions, UseConfirmDialogResult } from './hooks/useConfirmDialog'
90
89
 
90
+
91
91
  // Design tokens
92
92
  export {
93
93
  SPACING,
@@ -3,10 +3,7 @@ import { useColorScheme } from 'react-native'
3
3
  import { ThemeColors, Theme, ColorScheme, ThemeContextValue } from './types'
4
4
  import { defaultLight, defaultDark, deriveColors } from './colors'
5
5
 
6
- const ThemeContext = createContext<ThemeContextValue>({
7
- colors: deriveColors(defaultLight, 'light'),
8
- colorScheme: 'light',
9
- })
6
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
10
7
 
11
8
  export interface ThemeProviderProps {
12
9
  children: React.ReactNode
@@ -1,7 +1,4 @@
1
- // Hex color manipulation utilities for internal theme derivation.
2
- // All functions are pure — no side effects, no React dependencies.
3
-
4
- function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
1
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
5
2
  const clean = hex.replace('#', '')
6
3
  const full = clean.length === 3
7
4
  ? clean.split('').map(c => c + c).join('')
@@ -14,74 +11,6 @@ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
14
11
  }
15
12
  }
16
13
 
17
- function componentToHex(c: number): string {
18
- return Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')
19
- }
20
-
21
- function rgbToHex(r: number, g: number, b: number): string {
22
- return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`
23
- }
24
-
25
- // Returns hex color with alpha blended onto a white background (for tint derivation)
26
- export function withAlphaOnWhite(hex: string, alpha: number): string {
27
- const rgb = hexToRgb(hex)
28
- if (!rgb) return hex
29
- const r = rgb.r * alpha + 255 * (1 - alpha)
30
- const g = rgb.g * alpha + 255 * (1 - alpha)
31
- const b = rgb.b * alpha + 255 * (1 - alpha)
32
- return rgbToHex(r, g, b)
33
- }
34
-
35
- // Returns hex color with alpha blended onto a dark background (for dark mode tints)
36
- export function withAlphaOnDark(hex: string, alpha: number, bgHex = '#0f0f0f'): string {
37
- const rgb = hexToRgb(hex)
38
- const bg = hexToRgb(bgHex)
39
- if (!rgb || !bg) return hex
40
- const r = rgb.r * alpha + bg.r * (1 - alpha)
41
- const g = rgb.g * alpha + bg.g * (1 - alpha)
42
- const b = rgb.b * alpha + bg.b * (1 - alpha)
43
- return rgbToHex(r, g, b)
44
- }
45
-
46
- // Mix foreground color with background at given opacity (for text hierarchy)
47
- export function mixWithBackground(fgHex: string, bgHex: string, opacity: number): string {
48
- const fg = hexToRgb(fgHex)
49
- const bg = hexToRgb(bgHex)
50
- if (!fg || !bg) return fgHex
51
- const r = fg.r * opacity + bg.r * (1 - opacity)
52
- const g = fg.g * opacity + bg.g * (1 - opacity)
53
- const b = fg.b * opacity + bg.b * (1 - opacity)
54
- return rgbToHex(r, g, b)
55
- }
56
-
57
- // Lighten a hex color by mixing with white
58
- export function lighten(hex: string, amount: number): string {
59
- return withAlphaOnWhite(hex, 1 - amount)
60
- }
61
-
62
- // Darken a hex color by mixing with black
63
- export function darken(hex: string, amount: number): string {
64
- const rgb = hexToRgb(hex)
65
- if (!rgb) return hex
66
- return rgbToHex(rgb.r * (1 - amount), rgb.g * (1 - amount), rgb.b * (1 - amount))
67
- }
68
-
69
- // Detect if a hex color is "dark" (luminance < 0.5)
70
- export function isDark(hex: string): boolean {
71
- const rgb = hexToRgb(hex)
72
- if (!rgb) return false
73
- // Relative luminance (WCAG formula)
74
- const toLinear = (c: number) => {
75
- const s = c / 255
76
- return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
77
- }
78
- const L = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b)
79
- return L < 0.5
80
- }
81
-
82
- // Convert a hex color to rgba with the given alpha.
83
- // Returns an rgba() string suitable for use with semi-transparent backgrounds,
84
- // borders, and overlays.
85
14
  export function withAlpha(hex: string, alpha: number): string {
86
15
  const rgb = hexToRgb(hex)
87
16
  if (!rgb) return hex