@retray-dev/ui-kit 10.2.0 → 12.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 (220) hide show
  1. package/COMPONENTS.md +384 -40
  2. package/README.md +14 -5
  3. package/dist/Accordion.d.mts +6 -0
  4. package/dist/Accordion.d.ts +6 -0
  5. package/dist/Accordion.js +16 -0
  6. package/dist/Accordion.mjs +2 -2
  7. package/dist/AlertBanner.js +2 -0
  8. package/dist/AlertBanner.mjs +2 -2
  9. package/dist/AppHeader.js +2 -0
  10. package/dist/AppHeader.mjs +3 -3
  11. package/dist/Avatar.js +2 -0
  12. package/dist/Avatar.mjs +2 -2
  13. package/dist/Badge.js +2 -0
  14. package/dist/Badge.mjs +2 -2
  15. package/dist/Button.js +17 -17
  16. package/dist/Button.mjs +2 -2
  17. package/dist/Card.js +2 -0
  18. package/dist/Card.mjs +2 -2
  19. package/dist/CategoryStrip.js +2 -0
  20. package/dist/CategoryStrip.mjs +2 -2
  21. package/dist/Checkbox.js +2 -0
  22. package/dist/Checkbox.mjs +2 -2
  23. package/dist/Chip.js +2 -0
  24. package/dist/Chip.mjs +2 -2
  25. package/dist/ConfirmDialog.d.mts +1 -6
  26. package/dist/ConfirmDialog.d.ts +1 -6
  27. package/dist/ConfirmDialog.js +53 -41
  28. package/dist/ConfirmDialog.mjs +3 -3
  29. package/dist/CurrencyDisplay.js +2 -0
  30. package/dist/CurrencyDisplay.mjs +2 -2
  31. package/dist/CurrencyInput.d.mts +3 -8
  32. package/dist/CurrencyInput.d.ts +3 -8
  33. package/dist/CurrencyInput.js +5 -1
  34. package/dist/CurrencyInput.mjs +3 -3
  35. package/dist/DetailRow.js +2 -0
  36. package/dist/DetailRow.mjs +2 -2
  37. package/dist/EmptyState.js +17 -17
  38. package/dist/EmptyState.mjs +3 -3
  39. package/dist/ErrorBoundary.js +2 -0
  40. package/dist/ErrorBoundary.mjs +2 -2
  41. package/dist/Form.js +2 -0
  42. package/dist/Form.mjs +2 -2
  43. package/dist/IconButton.js +2 -0
  44. package/dist/IconButton.mjs +2 -2
  45. package/dist/IconPicker.js +677 -248
  46. package/dist/IconPicker.mjs +3 -2
  47. package/dist/ImageUpload.d.mts +3 -1
  48. package/dist/ImageUpload.d.ts +3 -1
  49. package/dist/ImageUpload.js +10 -3
  50. package/dist/ImageUpload.mjs +3 -3
  51. package/dist/ImageViewer.js +2 -0
  52. package/dist/ImageViewer.mjs +4 -4
  53. package/dist/Input.js +2 -0
  54. package/dist/Input.mjs +2 -2
  55. package/dist/LabelValue.js +2 -0
  56. package/dist/LabelValue.mjs +2 -2
  57. package/dist/ListGroup.js +2 -0
  58. package/dist/ListGroup.mjs +2 -2
  59. package/dist/ListItem.d.mts +7 -7
  60. package/dist/ListItem.d.ts +7 -7
  61. package/dist/ListItem.js +14 -7
  62. package/dist/ListItem.mjs +2 -2
  63. package/dist/MediaCard.js +2 -0
  64. package/dist/MediaCard.mjs +2 -2
  65. package/dist/MenuGroup.js +2 -0
  66. package/dist/MenuGroup.mjs +2 -2
  67. package/dist/MenuItem.js +2 -0
  68. package/dist/MenuItem.mjs +2 -2
  69. package/dist/MonthPicker.js +2 -0
  70. package/dist/MonthPicker.mjs +2 -2
  71. package/dist/NumberStepper.js +2 -0
  72. package/dist/NumberStepper.mjs +2 -2
  73. package/dist/PagerDots.js +2 -0
  74. package/dist/PagerDots.mjs +2 -2
  75. package/dist/Pressable.d.mts +15 -7
  76. package/dist/Pressable.d.ts +15 -7
  77. package/dist/Pressable.js +7 -3
  78. package/dist/Pressable.mjs +1 -1
  79. package/dist/PricingCard.js +17 -17
  80. package/dist/PricingCard.mjs +4 -4
  81. package/dist/Progress.js +2 -0
  82. package/dist/Progress.mjs +2 -2
  83. package/dist/RadioGroup.js +2 -0
  84. package/dist/RadioGroup.mjs +2 -2
  85. package/dist/RetrayProvider.d.mts +1 -1
  86. package/dist/RetrayProvider.d.ts +1 -1
  87. package/dist/RetrayProvider.js +2 -0
  88. package/dist/RetrayProvider.mjs +3 -3
  89. package/dist/Select.js +2 -0
  90. package/dist/Select.mjs +2 -2
  91. package/dist/SelectableCard.d.mts +27 -0
  92. package/dist/SelectableCard.d.ts +27 -0
  93. package/dist/SelectableCard.js +511 -0
  94. package/dist/SelectableCard.mjs +8 -0
  95. package/dist/SelectableGrid.js +2 -0
  96. package/dist/SelectableGrid.mjs +2 -2
  97. package/dist/Separator.js +2 -0
  98. package/dist/Separator.mjs +2 -2
  99. package/dist/Sheet.d.mts +4 -46
  100. package/dist/Sheet.d.ts +4 -46
  101. package/dist/Sheet.js +55 -115
  102. package/dist/Sheet.mjs +2 -3
  103. package/dist/SheetSelect.js +2 -0
  104. package/dist/SheetSelect.mjs +2 -2
  105. package/dist/Skeleton.d.mts +3 -1
  106. package/dist/Skeleton.d.ts +3 -1
  107. package/dist/Skeleton.js +5 -2
  108. package/dist/Skeleton.mjs +2 -2
  109. package/dist/Slider.js +2 -0
  110. package/dist/Slider.mjs +2 -2
  111. package/dist/Spinner.js +2 -0
  112. package/dist/Spinner.mjs +2 -2
  113. package/dist/Stats.d.mts +33 -0
  114. package/dist/Stats.d.ts +33 -0
  115. package/dist/Stats.js +453 -0
  116. package/dist/Stats.mjs +9 -0
  117. package/dist/Switch.js +2 -0
  118. package/dist/Switch.mjs +2 -2
  119. package/dist/TabBar.js +2 -0
  120. package/dist/TabBar.mjs +2 -2
  121. package/dist/Tabs.js +2 -0
  122. package/dist/Tabs.mjs +2 -2
  123. package/dist/Text.d.mts +3 -1
  124. package/dist/Text.d.ts +3 -1
  125. package/dist/Text.js +5 -3
  126. package/dist/Text.mjs +2 -2
  127. package/dist/Textarea.js +2 -0
  128. package/dist/Textarea.mjs +2 -2
  129. package/dist/Toast.js +2 -0
  130. package/dist/Toast.mjs +2 -2
  131. package/dist/Toggle.js +2 -0
  132. package/dist/Toggle.mjs +2 -2
  133. package/dist/{chunk-U2XJFYED.mjs → chunk-2BA3JMKK.mjs} +1 -1
  134. package/dist/{chunk-NMU5FMQJ.mjs → chunk-2HFD4IHU.mjs} +4 -2
  135. package/dist/{chunk-S2R7UVOE.mjs → chunk-2LG326TT.mjs} +1 -1
  136. package/dist/chunk-2P2CB235.mjs +236 -0
  137. package/dist/{chunk-6L4G6PBT.mjs → chunk-3XCFYSX4.mjs} +1 -1
  138. package/dist/{chunk-HTHGSXFG.mjs → chunk-4J2PXL36.mjs} +16 -18
  139. package/dist/{chunk-BEMIQXXU.mjs → chunk-4OORJ2DY.mjs} +1 -1
  140. package/dist/chunk-4XOB5TTD.mjs +166 -0
  141. package/dist/{chunk-FCSSQK3L.mjs → chunk-57V2LXCK.mjs} +1 -1
  142. package/dist/{chunk-6Q64UFIA.mjs → chunk-7AFZWSCI.mjs} +1 -1
  143. package/dist/{chunk-IX3NYLYQ.mjs → chunk-7ELGZ66G.mjs} +1 -1
  144. package/dist/{chunk-GD6KXMG5.mjs → chunk-AENAVIKT.mjs} +1 -1
  145. package/dist/{chunk-ID72TK46.mjs → chunk-BXF4AMHY.mjs} +1 -1
  146. package/dist/{chunk-SOA2Z4RB.mjs → chunk-C43HRKXH.mjs} +1 -1
  147. package/dist/{chunk-TZDGAP5N.mjs → chunk-CF27NBXO.mjs} +11 -6
  148. package/dist/{chunk-SXLKNTA4.mjs → chunk-DF7JA72E.mjs} +1 -1
  149. package/dist/{chunk-AJRVDP2H.mjs → chunk-E5UKLSJZ.mjs} +3 -3
  150. package/dist/{chunk-MBMXYJJV.mjs → chunk-E7NEHHXV.mjs} +7 -3
  151. package/dist/{chunk-VKID2D2I.mjs → chunk-EDLCGYIO.mjs} +13 -8
  152. package/dist/{chunk-BUMAMSTZ.mjs → chunk-ELGEOM7I.mjs} +1 -1
  153. package/dist/{chunk-DYT7BG5I.mjs → chunk-F3YTWO3T.mjs} +1 -1
  154. package/dist/{chunk-VF2ATYN3.mjs → chunk-GH67YXG6.mjs} +1 -1
  155. package/dist/{chunk-WJLKJMKR.mjs → chunk-GUTDFUNF.mjs} +4 -4
  156. package/dist/{chunk-6SECQ2ZF.mjs → chunk-HC4VVCWY.mjs} +2 -2
  157. package/dist/{chunk-A3A6KNQN.mjs → chunk-HEDQPK4I.mjs} +1 -1
  158. package/dist/{chunk-GQYFLP3D.mjs → chunk-IVSRW4HS.mjs} +1 -1
  159. package/dist/{chunk-KOO4WITD.mjs → chunk-KSUWPU2F.mjs} +1 -1
  160. package/dist/{chunk-WBOOUHSS.mjs → chunk-LIS6I5UP.mjs} +1 -1
  161. package/dist/{chunk-X4G6APW6.mjs → chunk-LNPKGWBG.mjs} +1 -1
  162. package/dist/{chunk-T2KCAHOS.mjs → chunk-LOBLCFMN.mjs} +1 -1
  163. package/dist/{chunk-ELXBDILQ.mjs → chunk-LPV4NJJK.mjs} +2 -2
  164. package/dist/{chunk-Y2NS74WS.mjs → chunk-M3C7XM2M.mjs} +53 -99
  165. package/dist/{chunk-BRKYVJVV.mjs → chunk-MEPSKGBO.mjs} +1 -1
  166. package/dist/{chunk-TBNZHU6C.mjs → chunk-MVMGPZN6.mjs} +2 -2
  167. package/dist/{chunk-YJ7I257J.mjs → chunk-NHDI3VQB.mjs} +15 -1
  168. package/dist/{chunk-Z6SFHN6T.mjs → chunk-NJG7DHVF.mjs} +1 -1
  169. package/dist/{chunk-RYZC432S.mjs → chunk-NLZY4TXU.mjs} +1 -1
  170. package/dist/{chunk-ZZ2R6KZ3.mjs → chunk-OLVJFKXS.mjs} +1 -1
  171. package/dist/{chunk-AJ7ZDNBT.mjs → chunk-QDAZGZUF.mjs} +4 -3
  172. package/dist/{chunk-JT7HKXRB.mjs → chunk-QOLWA2PW.mjs} +1 -1
  173. package/dist/{chunk-WYEUNUTP.mjs → chunk-QXDGGOLC.mjs} +38 -25
  174. package/dist/{chunk-JMOZEC77.mjs → chunk-RJNLAH76.mjs} +1 -1
  175. package/dist/{chunk-WF2XDFRK.mjs → chunk-RMRS44MQ.mjs} +1 -1
  176. package/dist/chunk-SAWUXP3A.mjs +1114 -0
  177. package/dist/{chunk-OB4JUQ3O.mjs → chunk-TS7DGUIR.mjs} +1 -1
  178. package/dist/{chunk-AV4EMIRH.mjs → chunk-UBUXUMER.mjs} +1 -1
  179. package/dist/{chunk-IRRY3CRZ.mjs → chunk-ULGNQPNE.mjs} +1 -1
  180. package/dist/{chunk-7LWRKMF5.mjs → chunk-UNNRUJTM.mjs} +1 -1
  181. package/dist/{chunk-TB6SD2FT.mjs → chunk-UQ4742ET.mjs} +1 -1
  182. package/dist/{chunk-MX6HRKMI.mjs → chunk-VJBUCITV.mjs} +1 -1
  183. package/dist/{chunk-2UYENBLV.mjs → chunk-YMYIEVZP.mjs} +1 -1
  184. package/dist/{chunk-SOYNZDVY.mjs → chunk-YTXRIXNZ.mjs} +8 -1
  185. package/dist/{chunk-YFZ3ELX5.mjs → chunk-ZIMY2QUM.mjs} +2 -2
  186. package/dist/{chunk-Z4VHZ7B5.mjs → chunk-ZR6HSEAB.mjs} +1 -1
  187. package/dist/fonts.d.mts +1 -7
  188. package/dist/fonts.d.ts +1 -7
  189. package/dist/fonts.js +0 -2
  190. package/dist/fonts.mjs +1 -2
  191. package/dist/{index-wt-orHUi.d.ts → index-CY34hxPN.d.mts} +1 -0
  192. package/dist/{index-wt-orHUi.d.mts → index-CY34hxPN.d.ts} +1 -0
  193. package/dist/index.d.mts +7 -3
  194. package/dist/index.d.ts +7 -3
  195. package/dist/index.js +1517 -761
  196. package/dist/index.mjs +54 -52
  197. package/package.json +3 -3
  198. package/src/components/Accordion/Accordion.tsx +20 -0
  199. package/src/components/Button/Button.tsx +29 -26
  200. package/src/components/ConfirmDialog/ConfirmDialog.tsx +47 -31
  201. package/src/components/CurrencyInput/CurrencyInput.tsx +4 -7
  202. package/src/components/IconPicker/IconPicker.tsx +124 -112
  203. package/src/components/ImageUpload/ImageUpload.tsx +10 -3
  204. package/src/components/ListItem/ListItem.tsx +43 -28
  205. package/src/components/Pressable/Pressable.tsx +20 -8
  206. package/src/components/SelectableCard/SelectableCard.tsx +304 -0
  207. package/src/components/SelectableCard/index.ts +1 -0
  208. package/src/components/Sheet/Sheet.tsx +72 -173
  209. package/src/components/Skeleton/Skeleton.tsx +5 -2
  210. package/src/components/Stats/Stats.tsx +254 -0
  211. package/src/components/Stats/index.ts +2 -0
  212. package/src/components/Text/Text.tsx +4 -2
  213. package/src/fonts.ts +0 -7
  214. package/src/index.ts +5 -0
  215. package/src/theme/colorUtils.ts +9 -0
  216. package/src/theme/colors.ts +7 -0
  217. package/src/theme/types.ts +4 -1
  218. package/src/utils/curatedIcons.ts +698 -135
  219. package/src/utils/fontGuard.ts +2 -1
  220. package/dist/chunk-53Z3NYGE.mjs +0 -742
@@ -24,6 +24,8 @@ export interface SkeletonProps {
24
24
  preset?: SkeletonPreset
25
25
  /** Only used with `preset='circle'` — overrides the diameter. Defaults to 40. */
26
26
  diameter?: number
27
+ /** Override the skeleton background color. Defaults to `colors.skeleton`. */
28
+ backgroundColor?: string
27
29
  style?: ViewStyle
28
30
  }
29
31
 
@@ -33,6 +35,7 @@ export function Skeleton({
33
35
  borderRadius = 6,
34
36
  preset = 'base',
35
37
  diameter = 40,
38
+ backgroundColor,
36
39
  style,
37
40
  }: SkeletonProps) {
38
41
  const { colors, colorScheme } = useTheme()
@@ -40,7 +43,7 @@ export function Skeleton({
40
43
  const [containerWidth, setContainerWidth] = useState(300)
41
44
 
42
45
  const shimmerHighlight =
43
- colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.7)'
46
+ colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.07)'
44
47
 
45
48
  useEffect(() => {
46
49
  // Repeats indefinitely on the UI thread — zero JS bridge cost per frame.
@@ -75,7 +78,7 @@ export function Skeleton({
75
78
  <View
76
79
  style={[
77
80
  styles.base,
78
- { width: resolvedWidth as number | `${number}%`, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: colors.surface },
81
+ { width: resolvedWidth as number | `${number}%`, height: resolvedHeight, borderRadius: resolvedRadius, backgroundColor: backgroundColor ?? colors.skeleton },
79
82
  style,
80
83
  ]}
81
84
  onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
@@ -0,0 +1,254 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { View, Text, StyleSheet, ViewStyle, LayoutChangeEvent } from 'react-native'
3
+ import { impactLight } from '../../utils/haptics'
4
+ import { useTheme } from '../../theme'
5
+ import { s, vs, ms, mvs } from '../../utils/scaling'
6
+ import { RADIUS } from '../../tokens'
7
+ import { renderIcon } from '../../utils/icons'
8
+ import { PressableCard } from '../../utils/pressable'
9
+
10
+ export type StatsVariant = 'elevated' | 'outlined' | 'filled'
11
+ export type StatsSize = 'default' | 'compact'
12
+
13
+ export interface StatsProps {
14
+ value: string
15
+ label: string
16
+ description?: string
17
+ icon?: React.ReactNode
18
+ iconName?: string
19
+ iconColor?: string
20
+ variant?: StatsVariant
21
+ /** `'compact'` reduces everything proportionally for tight grids. */
22
+ size?: StatsSize
23
+ onPress?: () => void
24
+ style?: ViewStyle
25
+ accessibilityLabel?: string
26
+ }
27
+
28
+ export interface StatsGroupProps {
29
+ children: React.ReactNode
30
+ gap?: number
31
+ style?: ViewStyle
32
+ }
33
+
34
+ const COMPACT_THRESHOLD = s(150)
35
+
36
+ function StatsComponent({
37
+ value,
38
+ label,
39
+ description,
40
+ icon,
41
+ iconName,
42
+ iconColor,
43
+ variant = 'elevated',
44
+ size = 'default',
45
+ onPress,
46
+ style,
47
+ accessibilityLabel,
48
+ }: StatsProps) {
49
+ const { colors } = useTheme()
50
+ const [containerWidth, setContainerWidth] = useState(0)
51
+
52
+ const handleLayout = useCallback((e: LayoutChangeEvent) => {
53
+ const w = e.nativeEvent.layout.width
54
+ if (w > 0 && w !== containerWidth) {
55
+ setContainerWidth(w)
56
+ }
57
+ }, [containerWidth])
58
+
59
+ const handlePress = () => {
60
+ if (!onPress) return
61
+ impactLight()
62
+ onPress()
63
+ }
64
+
65
+ const isCompact = containerWidth > 0 && containerWidth < COMPACT_THRESHOLD && !!(icon ?? iconName)
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
+
91
+ const variantStyle: ViewStyle = {
92
+ elevated: {
93
+ backgroundColor: colors.card,
94
+ borderWidth: 0,
95
+ shadowColor: '#000',
96
+ shadowOffset: { width: 0, height: 4 },
97
+ shadowOpacity: 0.09,
98
+ shadowRadius: 14,
99
+ elevation: 4,
100
+ },
101
+ outlined: {
102
+ backgroundColor: colors.card,
103
+ borderColor: colors.border,
104
+ shadowOpacity: 0,
105
+ elevation: 0,
106
+ },
107
+ filled: {
108
+ backgroundColor: colors.surfaceStrong,
109
+ borderColor: colors.border,
110
+ shadowOpacity: 0,
111
+ elevation: 0,
112
+ },
113
+ }[variant]
114
+
115
+ const iconColorResolved = iconColor ?? colors.primary
116
+
117
+ const resolvedIcon = iconName ? renderIcon(iconName, sizeStyles.iconSize, iconColorResolved) : icon
118
+
119
+ const iconElement = resolvedIcon ? (
120
+ <View style={styles.iconWrapper}>{resolvedIcon}</View>
121
+ ) : null
122
+
123
+ const valueElement = (
124
+ <Text style={[styles.value, { color: colors.foreground, fontFamily: sizeStyles.valueFontFamily, fontSize: sizeStyles.valueFontSize, lineHeight: sizeStyles.valueLineHeight }]} allowFontScaling={true}>
125
+ {value}
126
+ </Text>
127
+ )
128
+
129
+ const cardContent = (
130
+ <View style={[styles.card, variantStyle, { padding: sizeStyles.padding }, style]} onLayout={handleLayout}>
131
+ {isCompact ? (
132
+ <>
133
+ {iconElement}
134
+ <View style={styles.compactValue}>{valueElement}</View>
135
+ <Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
136
+ {label}
137
+ </Text>
138
+ {description ? (
139
+ <Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
140
+ {description}
141
+ </Text>
142
+ ) : null}
143
+ </>
144
+ ) : (
145
+ <>
146
+ <View style={styles.valueRow}>
147
+ {iconElement}
148
+ {valueElement}
149
+ </View>
150
+ <Text style={[styles.label, { color: colors.foregroundSubtle, fontSize: sizeStyles.labelFontSize, lineHeight: sizeStyles.labelLineHeight }]} allowFontScaling={true}>
151
+ {label}
152
+ </Text>
153
+ {description ? (
154
+ <Text style={[styles.description, { color: colors.foregroundMuted, fontSize: sizeStyles.descriptionFontSize, lineHeight: sizeStyles.descriptionLineHeight }]} allowFontScaling={true}>
155
+ {description}
156
+ </Text>
157
+ ) : null}
158
+ </>
159
+ )}
160
+ </View>
161
+ )
162
+
163
+ if (onPress) {
164
+ return (
165
+ <PressableCard
166
+ onPress={handlePress}
167
+ rippleColor="transparent"
168
+ touchSoundDisabled
169
+ activateOnHover
170
+ accessibilityRole="button"
171
+ accessibilityLabel={accessibilityLabel}
172
+ >
173
+ {cardContent}
174
+ </PressableCard>
175
+ )
176
+ }
177
+
178
+ return cardContent
179
+ }
180
+
181
+ function StatsGroup({ children, gap = s(12), style }: StatsGroupProps) {
182
+ return (
183
+ <View style={[styles.group, { gap }, style]}>
184
+ {React.Children.map(children, (child) => {
185
+ if (!React.isValidElement(child)) return child
186
+ const childStyle = (child.props as { style?: ViewStyle }).style
187
+ const mergedStyle = childStyle
188
+ ? [childStyle, { alignSelf: 'stretch' as const }]
189
+ : [{ alignSelf: 'stretch' as const }]
190
+ return (
191
+ <View style={styles.groupItem}>
192
+ {React.cloneElement(
193
+ child as React.ReactElement<{ style?: ViewStyle }>,
194
+ { style: mergedStyle as unknown as ViewStyle },
195
+ )}
196
+ </View>
197
+ )
198
+ })}
199
+ </View>
200
+ )
201
+ }
202
+
203
+ export const Stats = Object.assign(React.memo(StatsComponent), { Group: StatsGroup })
204
+
205
+ const styles = StyleSheet.create({
206
+ card: {
207
+ borderRadius: RADIUS.md,
208
+ borderWidth: 1,
209
+ padding: s(16),
210
+ alignSelf: 'flex-start',
211
+ alignItems: 'center',
212
+ justifyContent: 'center',
213
+ },
214
+ valueRow: {
215
+ flexDirection: 'row',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ gap: s(8),
219
+ },
220
+ iconWrapper: {
221
+ alignItems: 'center',
222
+ justifyContent: 'center',
223
+ },
224
+ compactValue: {
225
+ marginTop: vs(8),
226
+ },
227
+ value: {
228
+ fontFamily: 'Sohne-Bold',
229
+ fontSize: ms(28),
230
+ lineHeight: mvs(32),
231
+ textAlign: 'center',
232
+ },
233
+ label: {
234
+ fontFamily: 'Sohne-Regular',
235
+ fontSize: ms(13),
236
+ lineHeight: mvs(18),
237
+ marginTop: vs(8),
238
+ textAlign: 'center',
239
+ },
240
+ description: {
241
+ fontFamily: 'Sohne-Regular',
242
+ fontSize: ms(12),
243
+ lineHeight: mvs(16),
244
+ marginTop: vs(4),
245
+ textAlign: 'center',
246
+ },
247
+ group: {
248
+ flexDirection: 'row',
249
+ width: '100%',
250
+ },
251
+ groupItem: {
252
+ flex: 1,
253
+ },
254
+ })
@@ -0,0 +1,2 @@
1
+ export { Stats } from './Stats'
2
+ export type { StatsProps, StatsVariant, StatsGroupProps } from './Stats'
@@ -26,6 +26,8 @@ export type TextVariant =
26
26
  export interface TextProps extends RNTextProps {
27
27
  variant?: TextVariant
28
28
  color?: string
29
+ /** Force uppercase text transformation. Useful for labels, headers, and buttons. */
30
+ uppercase?: boolean
29
31
  }
30
32
 
31
33
  // Apply scaling to font/line-height values while preserving all other token props
@@ -68,7 +70,7 @@ const defaultColorVariant: Partial<Record<TextVariant, 'foreground' | 'foregroun
68
70
  'button-sm': 'foreground',
69
71
  }
70
72
 
71
- function TextBase({ variant = 'body-md', color, style, children, ...props }: TextProps) {
73
+ function TextBase({ variant = 'body-md', color, style, uppercase, children, ...props }: TextProps) {
72
74
  warnIfFontsMissing()
73
75
  const { colors } = useTheme()
74
76
 
@@ -77,7 +79,7 @@ function TextBase({ variant = 'body-md', color, style, children, ...props }: Tex
77
79
 
78
80
  return (
79
81
  <RNText
80
- style={[variantStyles[variant], { color: resolvedColor }, style]}
82
+ style={[variantStyles[variant], { color: resolvedColor }, uppercase && { textTransform: 'uppercase' }, style]}
81
83
  allowFontScaling={true}
82
84
  {...props}
83
85
  >
package/src/fonts.ts CHANGED
@@ -63,10 +63,3 @@ export const SohneFontNames = [
63
63
 
64
64
  /** Type for any valid Sohne font family name */
65
65
  export type SohneFontName = (typeof SohneFontNames)[number]
66
-
67
- /**
68
- * @deprecated SohneFonts export removed in v10.0.0.
69
- * Metro cannot resolve require() calls from node_modules reliably.
70
- * Copy the static SohneFonts boilerplate from CONSUMER.md into your App.tsx instead.
71
- */
72
- export const SohneFonts = undefined
package/src/index.ts CHANGED
@@ -50,6 +50,7 @@ export * from './components/ErrorBoundary'
50
50
  export * from './components/PagerDots'
51
51
  export * from './components/AppHeader'
52
52
  export * from './components/SelectableGrid'
53
+ export * from './components/SelectableCard'
53
54
  export * from './components/PricingCard'
54
55
  export * from './components/TabBar'
55
56
  export * from './components/ImageViewer'
@@ -57,6 +58,7 @@ export * from './components/SheetSelect'
57
58
  export * from './components/ImageUpload'
58
59
  export * from './components/IconPicker'
59
60
  export * from './components/NumberStepper'
61
+ export * from './components/Stats'
60
62
  // HolographicCard is intentionally NOT re-exported here — it depends on the
61
63
  // optional peer @shopify/react-native-skia, so it must stay out of the main
62
64
  // barrel's module graph. Deep-import it: '@retray-dev/ui-kit/HolographicCard'.
@@ -64,6 +66,9 @@ export * from './components/NumberStepper'
64
66
  // Icon utility
65
67
  export { Icon, renderIcon, configureIconFamilies, getValidIconNames } from './utils/icons'
66
68
 
69
+ // Color utilities
70
+ export { withAlpha } from './theme/colorUtils'
71
+
67
72
  // Typography utilities
68
73
  export { getResponsiveFontSize } from './utils/typography'
69
74
  export type { IconProps, IconFamily } from './utils/icons'
@@ -78,3 +78,12 @@ export function isDark(hex: string): boolean {
78
78
  const L = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b)
79
79
  return L < 0.5
80
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
+ export function withAlpha(hex: string, alpha: number): string {
86
+ const rgb = hexToRgb(hex)
87
+ if (!rgb) return hex
88
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`
89
+ }
@@ -69,6 +69,12 @@ export function deriveColors(t: ThemeColors, scheme: 'light' | 'dark'): Resolved
69
69
  ? lighten(bg, -0.12)
70
70
  : darken(bg, 0.08)
71
71
 
72
+ // Skeleton needs higher contrast than surface to be visible.
73
+ // Light: 10% darken (was 4% via surface — invisible). Dark: 10% lighten.
74
+ const skeleton = dark
75
+ ? lighten(bg, -0.10)
76
+ : darken(bg, 0.10)
77
+
72
78
  const destructiveTint = dark
73
79
  ? withAlphaOnDark(t.destructive, 0.15, bg)
74
80
  : withAlphaOnWhite(t.destructive, 0.08)
@@ -96,6 +102,7 @@ export function deriveColors(t: ThemeColors, scheme: 'light' | 'dark'): Resolved
96
102
  foregroundMuted,
97
103
  surface,
98
104
  surfaceStrong,
105
+ skeleton,
99
106
  destructiveTint,
100
107
  destructiveBorder,
101
108
  successTint,
@@ -29,10 +29,13 @@ export type ResolvedColors = ThemeColors & {
29
29
  foregroundSubtle: string // ~55% — body text, subtitles
30
30
  foregroundMuted: string // ~35% — captions, timestamps, placeholders
31
31
 
32
- // Surface fills (chips unselected, input bg, tag bg, skeleton)
32
+ // Surface fills (chips unselected, input bg, tag bg)
33
33
  surface: string // background slightly off-canvas
34
34
  surfaceStrong: string // slightly stronger fill for pressed/hover states
35
35
 
36
+ // Skeleton placeholder — higher contrast than surface for visibility
37
+ skeleton: string
38
+
36
39
  // Semantic tints (light bg for alert banners, toast backgrounds)
37
40
  destructiveTint: string
38
41
  destructiveBorder: string