@expokit/cli 0.0.0-stage → 0.3.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/bin/expo-kit.js +1266 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +345 -0
  6. package/package.json +49 -4
  7. package/registry/_docs/accordion.json +115 -0
  8. package/registry/_docs/action-sheet.json +138 -0
  9. package/registry/_docs/alert.json +155 -0
  10. package/registry/_docs/avatar.json +227 -0
  11. package/registry/_docs/badge.json +148 -0
  12. package/registry/_docs/bottom-nav.json +143 -0
  13. package/registry/_docs/bottom-sheet.json +186 -0
  14. package/registry/_docs/bullet.json +27 -0
  15. package/registry/_docs/button-3d.json +138 -0
  16. package/registry/_docs/button.json +131 -0
  17. package/registry/_docs/calendar.json +198 -0
  18. package/registry/_docs/card-stack.json +124 -0
  19. package/registry/_docs/card.json +131 -0
  20. package/registry/_docs/carousel.json +122 -0
  21. package/registry/_docs/chat.json +184 -0
  22. package/registry/_docs/checkbox.json +109 -0
  23. package/registry/_docs/checkout.json +105 -0
  24. package/registry/_docs/chip-group.json +132 -0
  25. package/registry/_docs/color-swatch.json +82 -0
  26. package/registry/_docs/confetti.json +102 -0
  27. package/registry/_docs/coupon.json +192 -0
  28. package/registry/_docs/cube-pager.json +100 -0
  29. package/registry/_docs/dashboard.json +123 -0
  30. package/registry/_docs/date-picker.json +269 -0
  31. package/registry/_docs/dialog.json +186 -0
  32. package/registry/_docs/drawer.json +199 -0
  33. package/registry/_docs/fab.json +144 -0
  34. package/registry/_docs/flip-card.json +127 -0
  35. package/registry/_docs/floating-input.json +211 -0
  36. package/registry/_docs/gift-card.json +124 -0
  37. package/registry/_docs/glass-button.json +142 -0
  38. package/registry/_docs/glass-nav-bar.json +102 -0
  39. package/registry/_docs/glass-search-bar.json +114 -0
  40. package/registry/_docs/glass-segmented.json +94 -0
  41. package/registry/_docs/glass-sheet.json +136 -0
  42. package/registry/_docs/glass-tab-bar.json +100 -0
  43. package/registry/_docs/glass.json +94 -0
  44. package/registry/_docs/inbox.json +75 -0
  45. package/registry/_docs/isometric-bar.json +75 -0
  46. package/registry/_docs/kit.json +14 -0
  47. package/registry/_docs/kpi-card.json +14 -0
  48. package/registry/_docs/like-button.json +92 -0
  49. package/registry/_docs/liquidglass-tab.json +30 -0
  50. package/registry/_docs/list-item.json +216 -0
  51. package/registry/_docs/loyalty-card.json +124 -0
  52. package/registry/_docs/morph-button.json +128 -0
  53. package/registry/_docs/number-ticker.json +112 -0
  54. package/registry/_docs/offers.json +81 -0
  55. package/registry/_docs/onboarding.json +69 -0
  56. package/registry/_docs/otp-input.json +158 -0
  57. package/registry/_docs/pagination-dots.json +108 -0
  58. package/registry/_docs/parallax-layers.json +104 -0
  59. package/registry/_docs/parallax-scroll.json +210 -0
  60. package/registry/_docs/password-input.json +112 -0
  61. package/registry/_docs/payment-card.json +169 -0
  62. package/registry/_docs/pixel-bar.json +69 -0
  63. package/registry/_docs/polar-area.json +81 -0
  64. package/registry/_docs/profile.json +87 -0
  65. package/registry/_docs/progress-steps.json +116 -0
  66. package/registry/_docs/progress.json +152 -0
  67. package/registry/_docs/radio-group.json +112 -0
  68. package/registry/_docs/rating.json +114 -0
  69. package/registry/_docs/ring-carousel.json +122 -0
  70. package/registry/_docs/scratch-card.json +110 -0
  71. package/registry/_docs/search-bar.json +160 -0
  72. package/registry/_docs/search-results.json +123 -0
  73. package/registry/_docs/segmented-control.json +123 -0
  74. package/registry/_docs/select.json +277 -0
  75. package/registry/_docs/settings.json +75 -0
  76. package/registry/_docs/shimmer-button.json +98 -0
  77. package/registry/_docs/sign-in.json +201 -0
  78. package/registry/_docs/skeleton.json +162 -0
  79. package/registry/_docs/slide-to-confirm.json +108 -0
  80. package/registry/_docs/slider.json +177 -0
  81. package/registry/_docs/spin-wheel.json +138 -0
  82. package/registry/_docs/spinner.json +121 -0
  83. package/registry/_docs/stateful-button.json +184 -0
  84. package/registry/_docs/stepper.json +184 -0
  85. package/registry/_docs/swipe-deck.json +118 -0
  86. package/registry/_docs/swipeable-row.json +95 -0
  87. package/registry/_docs/switch.json +124 -0
  88. package/registry/_docs/tabs.json +129 -0
  89. package/registry/_docs/tag-input.json +148 -0
  90. package/registry/_docs/text-animation.json +141 -0
  91. package/registry/_docs/textarea.json +191 -0
  92. package/registry/_docs/ticket-card.json +194 -0
  93. package/registry/_docs/ticket-shape.json +14 -0
  94. package/registry/_docs/tilt-card.json +157 -0
  95. package/registry/_docs/time-picker.json +269 -0
  96. package/registry/_docs/timeline-chart.json +33 -0
  97. package/registry/_docs/timeline.json +172 -0
  98. package/registry/_docs/toast.json +185 -0
  99. package/registry/_docs/treemap.json +33 -0
  100. package/registry/_docs/waterfall.json +39 -0
  101. package/registry/_docs/wheel-list.json +122 -0
  102. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  103. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  104. package/registry/accordion.json +17 -0
  105. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  106. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  107. package/registry/action-sheet.json +18 -0
  108. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  109. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  110. package/registry/alert/files/src/lib/kit.tsx +219 -0
  111. package/registry/alert.json +20 -0
  112. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  113. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  114. package/registry/avatar.json +18 -0
  115. package/registry/badge/files/src/components/ui/badge.tsx +626 -0
  116. package/registry/badge/files/src/lib/kit.tsx +219 -0
  117. package/registry/badge.json +17 -0
  118. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  119. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-nav.json +18 -0
  121. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  122. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  123. package/registry/bottom-sheet.json +17 -0
  124. package/registry/bullet.json +14 -0
  125. package/registry/button/files/src/components/ui/button.tsx +274 -0
  126. package/registry/button/files/src/lib/kit.tsx +219 -0
  127. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  128. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  129. package/registry/button-3d.json +18 -0
  130. package/registry/button.json +18 -0
  131. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  132. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  133. package/registry/calendar.json +17 -0
  134. package/registry/card/files/src/components/ui/card.tsx +300 -0
  135. package/registry/card/files/src/lib/kit.tsx +219 -0
  136. package/registry/card-stack.json +15 -0
  137. package/registry/card.json +17 -0
  138. package/registry/carousel.json +15 -0
  139. package/registry/chat.json +18 -0
  140. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  141. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  142. package/registry/checkbox.json +17 -0
  143. package/registry/checkout.json +26 -0
  144. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  145. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  146. package/registry/chip-group.json +17 -0
  147. package/registry/color-swatch.json +17 -0
  148. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  149. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  150. package/registry/confetti.json +15 -0
  151. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  152. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  153. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  154. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  155. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  156. package/registry/coupon.json +23 -0
  157. package/registry/cube-pager.json +15 -0
  158. package/registry/dashboard.json +32 -0
  159. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  160. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  161. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  162. package/registry/date-picker.json +19 -0
  163. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  164. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  165. package/registry/dialog.json +18 -0
  166. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  167. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  168. package/registry/drawer.json +18 -0
  169. package/registry/fab.json +18 -0
  170. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  171. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  172. package/registry/flip-card.json +15 -0
  173. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  174. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  175. package/registry/floating-input.json +17 -0
  176. package/registry/gift-card.json +21 -0
  177. package/registry/glass-button.json +22 -0
  178. package/registry/glass-nav-bar.json +21 -0
  179. package/registry/glass-search-bar.json +21 -0
  180. package/registry/glass-segmented.json +22 -0
  181. package/registry/glass-sheet.json +22 -0
  182. package/registry/glass-tab-bar.json +22 -0
  183. package/registry/glass.json +19 -0
  184. package/registry/inbox.json +30 -0
  185. package/registry/isometric-bar.json +14 -0
  186. package/registry/kit/files/src/lib/kit.tsx +219 -0
  187. package/registry/kit.json +11 -0
  188. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  189. package/registry/kpi-card.json +14 -0
  190. package/registry/like-button.json +18 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  192. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  193. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  194. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  195. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  196. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  197. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  198. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  199. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  200. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  201. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  202. package/registry/liquidglass-tab.json +23 -0
  203. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  204. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  205. package/registry/list-item.json +17 -0
  206. package/registry/loyalty-card.json +23 -0
  207. package/registry/morph-button.json +18 -0
  208. package/registry/number-ticker.json +15 -0
  209. package/registry/offers.json +24 -0
  210. package/registry/onboarding.json +17 -0
  211. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  212. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  213. package/registry/otp-input.json +17 -0
  214. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  215. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  216. package/registry/pagination-dots.json +15 -0
  217. package/registry/parallax-layers.json +15 -0
  218. package/registry/parallax-scroll.json +17 -0
  219. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  220. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  221. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  222. package/registry/password-input.json +19 -0
  223. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  224. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  225. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  226. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  227. package/registry/payment-card.json +21 -0
  228. package/registry/pixel-bar.json +14 -0
  229. package/registry/polar-area.json +14 -0
  230. package/registry/profile.json +17 -0
  231. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  232. package/registry/progress/files/src/lib/kit.tsx +219 -0
  233. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  234. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  235. package/registry/progress-steps.json +18 -0
  236. package/registry/progress.json +17 -0
  237. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  238. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  239. package/registry/radio-group.json +18 -0
  240. package/registry/rating.json +18 -0
  241. package/registry/ring-carousel.json +15 -0
  242. package/registry/scratch-card.json +18 -0
  243. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  244. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  245. package/registry/search-bar.json +17 -0
  246. package/registry/search-results.json +30 -0
  247. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  248. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  249. package/registry/segmented-control.json +18 -0
  250. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  251. package/registry/select/files/src/lib/kit.tsx +219 -0
  252. package/registry/select.json +17 -0
  253. package/registry/settings.json +26 -0
  254. package/registry/shimmer-button.json +17 -0
  255. package/registry/sign-in.json +26 -0
  256. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  257. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  258. package/registry/skeleton.json +17 -0
  259. package/registry/slide-to-confirm.json +18 -0
  260. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  261. package/registry/slider/files/src/lib/kit.tsx +219 -0
  262. package/registry/slider.json +17 -0
  263. package/registry/spin-wheel.json +18 -0
  264. package/registry/spinner/files/src/components/ui/spinner.tsx +748 -0
  265. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  266. package/registry/spinner.json +17 -0
  267. package/registry/stateful-button/files/src/components/ui/stateful-button.tsx +532 -0
  268. package/registry/stateful-button/files/src/lib/kit.tsx +219 -0
  269. package/registry/stateful-button.json +18 -0
  270. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  271. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  272. package/registry/stepper.json +17 -0
  273. package/registry/swipe-deck.json +15 -0
  274. package/registry/swipeable-row.json +15 -0
  275. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  276. package/registry/switch/files/src/lib/kit.tsx +219 -0
  277. package/registry/switch.json +18 -0
  278. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  279. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  280. package/registry/tabs.json +17 -0
  281. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  282. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  283. package/registry/tag-input.json +17 -0
  284. package/registry/text-animation/files/src/components/ui/text-animation.tsx +650 -0
  285. package/registry/text-animation/files/src/lib/kit.tsx +219 -0
  286. package/registry/text-animation.json +15 -0
  287. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  288. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  289. package/registry/textarea.json +17 -0
  290. package/registry/ticket-card.json +24 -0
  291. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  292. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  293. package/registry/ticket-shape.json +17 -0
  294. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  295. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  296. package/registry/tilt-card.json +17 -0
  297. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  298. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  299. package/registry/time-picker.json +17 -0
  300. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  301. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  302. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  303. package/registry/timeline-chart.json +14 -0
  304. package/registry/timeline.json +20 -0
  305. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  306. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  307. package/registry/toast/files/src/lib/kit.tsx +219 -0
  308. package/registry/toast.json +20 -0
  309. package/registry/treemap.json +14 -0
  310. package/registry/waterfall.json +14 -0
  311. package/registry/wheel-list.json +17 -0
  312. package/schema/components.schema.json +13 -0
@@ -0,0 +1,508 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Image,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type StyleProp,
10
+ type TextStyle,
11
+ type ViewStyle,
12
+ } from 'react-native';
13
+ import Svg, { Circle, Path, Rect } from 'react-native-svg';
14
+
15
+ import { alpha, isTextNode, readableOn, useKit } from '@/lib/kit';
16
+
17
+ export type ListItemSize = 'sm' | 'md' | 'lg';
18
+
19
+ export type ListItemIconName =
20
+ | 'user'
21
+ | 'bell'
22
+ | 'lock'
23
+ | 'shield'
24
+ | 'mail'
25
+ | 'card'
26
+ | 'help'
27
+ | 'logout'
28
+ | 'moon'
29
+ | 'globe'
30
+ | 'star'
31
+ | 'trash'
32
+ | 'settings'
33
+ | 'heart'
34
+ | 'phone'
35
+ | 'wifi';
36
+
37
+ export interface ListItemIconProps {
38
+ color: string;
39
+ size: number;
40
+ }
41
+
42
+ export interface ListItemAvatar {
43
+ /** Image URL. Falls back to initials while loading or when it fails. */
44
+ src?: string;
45
+ /** Used for the initials and the fallback colour. */
46
+ name?: string;
47
+ /** Fallback circle colour (default: picked from the name). */
48
+ color?: string;
49
+ /** Shows a green presence dot. */
50
+ online?: boolean;
51
+ }
52
+
53
+ export interface ListItemProps {
54
+ title: string;
55
+ /** One muted line under the title. */
56
+ subtitle?: string;
57
+ /** Longer muted text under the subtitle, clamped to `numberOfLines`. */
58
+ description?: string;
59
+ /** Lines shown of `description` (default 2). */
60
+ numberOfLines?: number;
61
+ /** Small text at the right end of the title row, e.g. a time ("9:41"). */
62
+ meta?: string;
63
+
64
+ /** Built-in icon name or your own icon, drawn in a rounded tile. */
65
+ icon?: ListItemIconName | ((props: ListItemIconProps) => React.ReactNode);
66
+ /** Tile colour (default: theme accent; the danger colour when `destructive`). */
67
+ iconColor?: string;
68
+ /** 'filled' white glyph on a solid tile (iOS Settings) · 'tinted' coloured glyph on a soft tile. */
69
+ iconVariant?: 'filled' | 'tinted';
70
+ /** Round avatar instead of an icon tile. */
71
+ avatar?: ListItemAvatar;
72
+ /** Anything else on the left (overrides `icon` / `avatar`). */
73
+ leading?: React.ReactNode;
74
+
75
+ /** Muted value text on the right, e.g. "English" or "1.2 GB". */
76
+ value?: string;
77
+ /** `true` draws a dot; a number or string draws a pill (99+ above 99). */
78
+ badge?: boolean | number | string;
79
+ /** Shows a chevron. Defaults to true when the row is pressable and has no `trailing` / `checked`. */
80
+ chevron?: boolean;
81
+ /** Shows an accent check mark (for pickers). */
82
+ checked?: boolean;
83
+ /** Anything on the right, e.g. a <Switch />. Rendered after `value` and `badge`. */
84
+ trailing?: React.ReactNode;
85
+
86
+ onPress?: () => void;
87
+ onLongPress?: () => void;
88
+ /** Red title and icon, for "Sign out" or "Delete account". */
89
+ destructive?: boolean;
90
+ disabled?: boolean;
91
+ size?: ListItemSize;
92
+ /** Hairline under the row that starts past the leading slot. ListGroup / ListSection set it for you. */
93
+ divider?: boolean;
94
+ /** Highlight colour for checks, badges and default icon tiles (default: theme accent). */
95
+ accent?: string;
96
+ titleStyle?: StyleProp<TextStyle>;
97
+ accessibilityLabel?: string;
98
+ style?: StyleProp<ViewStyle>;
99
+ }
100
+
101
+ const SIZES: Record<ListItemSize, { minH: number; tile: number; glyph: number; avatar: number; title: number; sub: number; padY: number }> = {
102
+ sm: { minH: 44, tile: 26, glyph: 15, avatar: 32, title: 15, sub: 13, padY: 8 },
103
+ md: { minH: 52, tile: 30, glyph: 18, avatar: 40, title: 16, sub: 13.5, padY: 11 },
104
+ lg: { minH: 68, tile: 38, glyph: 21, avatar: 48, title: 17, sub: 14, padY: 14 },
105
+ };
106
+
107
+ const PAD_X = 16;
108
+ const GAP = 12;
109
+ const AVATAR_COLORS = ['#f97316', '#0ea5e9', '#22c55e', '#a855f7', '#ec4899', '#14b8a6', '#6366f1', '#eab308'];
110
+
111
+ /**
112
+ * Settings / contacts style row: icon tile, avatar or custom leading; title, subtitle and
113
+ * description; value, badge, check, chevron or any control on the right. Pressing fades in a
114
+ * highlight on the native driver and fades it out slowly, like UIKit table cells.
115
+ */
116
+ export function ListItem({
117
+ title,
118
+ subtitle,
119
+ description,
120
+ numberOfLines = 2,
121
+ meta,
122
+ icon,
123
+ iconColor,
124
+ iconVariant = 'filled',
125
+ avatar,
126
+ leading,
127
+ value,
128
+ badge,
129
+ chevron,
130
+ checked,
131
+ trailing,
132
+ onPress,
133
+ onLongPress,
134
+ destructive,
135
+ disabled,
136
+ size = 'md',
137
+ divider,
138
+ accent,
139
+ titleStyle,
140
+ accessibilityLabel,
141
+ style,
142
+ }: ListItemProps) {
143
+ const { colors, radius } = useKit();
144
+ const tint = accent ?? colors.accent;
145
+ const s = SIZES[size];
146
+ const pressable = !!(onPress || onLongPress);
147
+ const showChevron = chevron ?? (pressable && trailing === undefined && checked === undefined);
148
+ const titleColor = destructive ? colors.danger : colors.foreground;
149
+
150
+ // Press highlight: an overlay whose opacity runs on the native driver.
151
+ const highlight = useRef(new Animated.Value(0)).current;
152
+ const fade = (to: number, duration: number) =>
153
+ Animated.timing(highlight, { toValue: to, duration, useNativeDriver: true }).start();
154
+
155
+ // ---- leading -------------------------------------------------------------------------------
156
+ let lead: React.ReactNode = leading ?? null;
157
+ let leadW = 0;
158
+ if (leading) {
159
+ leadW = -1; // unknown width: divider starts at the text via onLayout below
160
+ } else if (avatar) {
161
+ lead = <AvatarCircle {...avatar} size={s.avatar} ring={colors.card} />;
162
+ leadW = s.avatar;
163
+ } else if (icon) {
164
+ const tile = iconColor ?? (destructive ? colors.danger : tint);
165
+ const filled = iconVariant === 'filled';
166
+ const glyphColor = filled ? readableOn(tile) : tile;
167
+ lead = (
168
+ <View
169
+ style={[
170
+ styles.tile,
171
+ { width: s.tile, height: s.tile, borderRadius: Math.min(s.tile / 2, radius.scale(Math.round(s.tile * 0.27))), backgroundColor: filled ? tile : alpha(tile, 0.14) },
172
+ ]}
173
+ >
174
+ {typeof icon === 'function' ? icon({ color: glyphColor, size: s.glyph }) : <ListIcon name={icon} color={glyphColor} size={s.glyph} />}
175
+ </View>
176
+ );
177
+ leadW = s.tile;
178
+ }
179
+
180
+ // Custom leading content: measure it so the divider still lines up with the text.
181
+ const [measured, setMeasured] = useState(0);
182
+ const inset = PAD_X + (leadW === -1 ? measured : leadW ? leadW + GAP : 0);
183
+
184
+ const badgeText = typeof badge === 'number' ? (badge > 99 ? '99+' : String(badge)) : typeof badge === 'string' ? badge : null;
185
+
186
+ const content = (
187
+ <>
188
+ {lead ? (
189
+ <View onLayout={leadW === -1 ? (e) => setMeasured(e.nativeEvent.layout.width + GAP) : undefined} style={styles.leading}>
190
+ {lead}
191
+ </View>
192
+ ) : null}
193
+ <View style={styles.body}>
194
+ <View style={styles.titleRow}>
195
+ <Text numberOfLines={1} style={[styles.title, { fontSize: s.title, color: titleColor }, titleStyle]}>
196
+ {title}
197
+ </Text>
198
+ {meta ? <Text style={[styles.meta, { color: colors.mutedForeground }]}>{meta}</Text> : null}
199
+ </View>
200
+ {subtitle ? (
201
+ <Text numberOfLines={1} style={[styles.sub, { fontSize: s.sub, color: colors.mutedForeground }]}>
202
+ {subtitle}
203
+ </Text>
204
+ ) : null}
205
+ {description ? (
206
+ <Text numberOfLines={numberOfLines} style={[styles.desc, { fontSize: s.sub, lineHeight: s.sub * 1.36, color: colors.mutedForeground }]}>
207
+ {description}
208
+ </Text>
209
+ ) : null}
210
+ </View>
211
+ {value || badge || checked || trailing !== undefined || showChevron ? (
212
+ <View style={styles.trailing}>
213
+ {value ? (
214
+ <Text numberOfLines={1} style={[styles.value, { color: colors.mutedForeground }]}>
215
+ {value}
216
+ </Text>
217
+ ) : null}
218
+ {badge === true ? <View style={[styles.dot, { backgroundColor: tint }]} /> : null}
219
+ {badgeText ? (
220
+ <View style={[styles.badge, { borderRadius: radius.capsule(22), backgroundColor: tint }]}>
221
+ <Text style={[styles.badgeText, { color: readableOn(tint) }]}>{badgeText}</Text>
222
+ </View>
223
+ ) : null}
224
+ {isTextNode(trailing) ? <Text style={[styles.value, { color: colors.mutedForeground }]}>{trailing}</Text> : trailing}
225
+ {checked ? <Check color={tint} /> : null}
226
+ {showChevron ? <Chevron color={alpha(colors.mutedForeground, 0.7)} /> : null}
227
+ </View>
228
+ ) : null}
229
+ {divider ? (
230
+ <View pointerEvents="none" style={[styles.divider, { left: inset, backgroundColor: colors.border }]} />
231
+ ) : null}
232
+ </>
233
+ );
234
+
235
+ const rowStyle = [styles.row, { minHeight: s.minH, paddingVertical: s.padY }, disabled && styles.disabled, style];
236
+
237
+ if (!pressable) {
238
+ return (
239
+ <View accessible accessibilityLabel={accessibilityLabel} style={rowStyle}>
240
+ {content}
241
+ </View>
242
+ );
243
+ }
244
+
245
+ return (
246
+ <Pressable
247
+ onPress={onPress}
248
+ onLongPress={onLongPress}
249
+ disabled={disabled}
250
+ onPressIn={() => fade(1, 60)}
251
+ onPressOut={() => fade(0, 320)}
252
+ accessibilityRole="button"
253
+ accessibilityState={{ disabled: !!disabled, checked: checked === undefined ? undefined : checked }}
254
+ accessibilityLabel={accessibilityLabel ?? [title, subtitle, value, badgeText ? `${badgeText} new` : null].filter(Boolean).join(', ')}
255
+ style={rowStyle}
256
+ >
257
+ <Animated.View
258
+ pointerEvents="none"
259
+ style={[
260
+ StyleSheet.absoluteFill,
261
+ { backgroundColor: destructive ? alpha(colors.danger, 0.1) : alpha(colors.foreground, 0.07), opacity: highlight },
262
+ ]}
263
+ />
264
+ {content}
265
+ </Pressable>
266
+ );
267
+ }
268
+
269
+ export interface ListGroupProps {
270
+ /** Small uppercase heading above the card. */
271
+ header?: string;
272
+ /** Muted explanation under the card. */
273
+ footer?: string;
274
+ /** Card colour (default: theme card). */
275
+ background?: string;
276
+ children: React.ReactNode;
277
+ style?: StyleProp<ViewStyle>;
278
+ }
279
+
280
+ /**
281
+ * iOS inset-grouped card: rounded surface with a header and footer, and hairline separators
282
+ * between rows that start past the icon so they line up with the text.
283
+ */
284
+ export function ListGroup({ header, footer, background, children, style }: ListGroupProps) {
285
+ const { colors, radius } = useKit();
286
+ return (
287
+ <View style={[styles.group, style]}>
288
+ {header ? (
289
+ <Text accessibilityRole="header" style={[styles.header, { color: colors.mutedForeground }]}>
290
+ {header}
291
+ </Text>
292
+ ) : null}
293
+ <View
294
+ style={[
295
+ styles.card,
296
+ { backgroundColor: background ?? colors.card, borderColor: colors.border, borderRadius: radius.lg },
297
+ ]}
298
+ >
299
+ {withDividers(children)}
300
+ </View>
301
+ {footer ? <Text style={[styles.footer, { color: colors.mutedForeground }]}>{footer}</Text> : null}
302
+ </View>
303
+ );
304
+ }
305
+
306
+ export interface ListSectionProps {
307
+ /** Section heading, e.g. "A" in a contacts list or "Today" in an inbox. */
308
+ header?: string;
309
+ footer?: string;
310
+ /** Hairline under the last row too (default false). */
311
+ dividerAfterLast?: boolean;
312
+ children: React.ReactNode;
313
+ style?: StyleProp<ViewStyle>;
314
+ }
315
+
316
+ /** Plain full-width section: header text, then rows with inset separators. */
317
+ export function ListSection({ header, footer, dividerAfterLast, children, style }: ListSectionProps) {
318
+ const { colors } = useKit();
319
+ return (
320
+ <View style={style}>
321
+ {header ? (
322
+ <Text accessibilityRole="header" style={[styles.sectionHeader, { color: colors.mutedForeground }]}>
323
+ {header}
324
+ </Text>
325
+ ) : null}
326
+ {withDividers(children, dividerAfterLast)}
327
+ {footer ? <Text style={[styles.footer, { color: colors.mutedForeground }]}>{footer}</Text> : null}
328
+ </View>
329
+ );
330
+ }
331
+
332
+ /** Gives every ListItem but the last a divider (unless it sets `divider` itself). */
333
+ function withDividers(children: React.ReactNode, last = false) {
334
+ const items = React.Children.toArray(children).filter(React.isValidElement);
335
+ return items.map((child, i) => {
336
+ if (child.type !== ListItem) return child;
337
+ const el = child as React.ReactElement<ListItemProps>;
338
+ if (el.props.divider !== undefined) return el;
339
+ return React.cloneElement(el, { divider: last || i < items.length - 1 });
340
+ });
341
+ }
342
+
343
+ function AvatarCircle({ src, name = '', color, online, size, ring }: ListItemAvatar & { size: number; ring: string }) {
344
+ const { colors } = useKit();
345
+ const [failed, setFailed] = useState(false);
346
+ const initials = name
347
+ .split(/\s+/)
348
+ .filter(Boolean)
349
+ .slice(0, 2)
350
+ .map((w) => w[0]!.toUpperCase())
351
+ .join('');
352
+ let hash = 0;
353
+ for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0;
354
+ const bg = color ?? AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
355
+ const dot = Math.max(10, Math.round(size * 0.28));
356
+ return (
357
+ <View style={{ width: size, height: size }}>
358
+ <View style={[styles.avatar, { width: size, height: size, borderRadius: size / 2, backgroundColor: bg }]}>
359
+ <Text style={{ color: readableOn(bg), fontSize: size * 0.38, fontWeight: '400' }}>{initials}</Text>
360
+ {src && !failed ? (
361
+ <Image source={{ uri: src }} onError={() => setFailed(true)} style={[StyleSheet.absoluteFill, { borderRadius: size / 2 }]} />
362
+ ) : null}
363
+ </View>
364
+ {online ? (
365
+ <View
366
+ style={[
367
+ styles.online,
368
+ { width: dot, height: dot, borderRadius: dot / 2, borderColor: ring, backgroundColor: colors.success },
369
+ ]}
370
+ />
371
+ ) : null}
372
+ </View>
373
+ );
374
+ }
375
+
376
+ function Chevron({ color }: { color: string }) {
377
+ return (
378
+ <Svg width={14} height={14} viewBox="0 0 24 24" fill="none">
379
+ <Path d="M9 5l7 7-7 7" stroke={color} strokeWidth={2.6} strokeLinecap="round" strokeLinejoin="round" />
380
+ </Svg>
381
+ );
382
+ }
383
+
384
+ function Check({ color }: { color: string }) {
385
+ return (
386
+ <Svg width={18} height={18} viewBox="0 0 24 24" fill="none">
387
+ <Path d="M5 12.5l4.5 4.5L19 7.5" stroke={color} strokeWidth={2.6} strokeLinecap="round" strokeLinejoin="round" />
388
+ </Svg>
389
+ );
390
+ }
391
+
392
+ /** The built-in glyphs, 24×24 line icons with a 1.8 stroke. */
393
+ export function ListIcon({ name, color, size = 20 }: { name: ListItemIconName; color: string; size?: number }) {
394
+ const p = { stroke: color, strokeWidth: 1.8, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, fill: 'none' };
395
+ const paths: Record<ListItemIconName, React.ReactNode> = {
396
+ user: (
397
+ <>
398
+ <Circle cx={12} cy={8} r={4} {...p} />
399
+ <Path d="M4 20.5c1.2-3.6 4.3-5.5 8-5.5s6.8 1.9 8 5.5" {...p} />
400
+ </>
401
+ ),
402
+ bell: (
403
+ <>
404
+ <Path d="M6 16v-5a6 6 0 0 1 12 0v5l1.5 2h-15z" {...p} />
405
+ <Path d="M10 21h4" {...p} />
406
+ </>
407
+ ),
408
+ lock: (
409
+ <>
410
+ <Rect x={5} y={11} width={14} height={10} rx={2.5} {...p} />
411
+ <Path d="M8 11V8a4 4 0 0 1 8 0v3" {...p} />
412
+ </>
413
+ ),
414
+ shield: (
415
+ <>
416
+ <Path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z" {...p} />
417
+ <Path d="M9 12l2 2 4-4" {...p} />
418
+ </>
419
+ ),
420
+ mail: (
421
+ <>
422
+ <Rect x={3} y={5} width={18} height={14} rx={2.5} {...p} />
423
+ <Path d="M3.5 7l8.5 6 8.5-6" {...p} />
424
+ </>
425
+ ),
426
+ card: (
427
+ <>
428
+ <Rect x={3} y={5} width={18} height={14} rx={2.5} {...p} />
429
+ <Path d="M3 10h18M7 15h4" {...p} />
430
+ </>
431
+ ),
432
+ help: (
433
+ <>
434
+ <Circle cx={12} cy={12} r={9} {...p} />
435
+ <Path d="M9.5 9.5a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6v.3" {...p} />
436
+ <Circle cx={12} cy={17} r={1} fill={color} />
437
+ </>
438
+ ),
439
+ logout: (
440
+ <>
441
+ <Path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3" {...p} />
442
+ <Path d="M10 8l-4 4 4 4M6 12h10" {...p} />
443
+ </>
444
+ ),
445
+ moon: <Path d="M20 14.2A8 8 0 1 1 9.8 4a6.4 6.4 0 0 0 10.2 10.2z" {...p} />,
446
+ globe: (
447
+ <>
448
+ <Circle cx={12} cy={12} r={9} {...p} />
449
+ <Path d="M3 12h18M12 3c2.5 2.6 3.7 5.6 3.7 9s-1.2 6.4-3.7 9c-2.5-2.6-3.7-5.6-3.7-9S9.5 5.6 12 3z" {...p} />
450
+ </>
451
+ ),
452
+ star: <Path d="M12 3.5l2.6 5.4 5.9.8-4.3 4.1 1 5.8L12 16.8l-5.2 2.8 1-5.8-4.3-4.1 5.9-.8z" {...p} />,
453
+ trash: (
454
+ <>
455
+ <Path d="M4 7h16M10 11v6M14 11v6M9 7V4.5h6V7" {...p} />
456
+ <Path d="M6 7l1 13h10l1-13" {...p} />
457
+ </>
458
+ ),
459
+ settings: (
460
+ <>
461
+ <Circle cx={12} cy={12} r={3} {...p} />
462
+ <Path
463
+ d="M12 2.8l1.6 2.3 2.7-.6.6 2.7 2.3 1.6-1.2 2.5 1.2 2.5-2.3 1.6-.6 2.7-2.7-.6L12 21.2l-1.6-2.3-2.7.6-.6-2.7-2.3-1.6L6 12.7 4.8 10.2l2.3-1.6.6-2.7 2.7.6z"
464
+ {...p}
465
+ />
466
+ </>
467
+ ),
468
+ heart: <Path d="M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 7.4 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10z" {...p} />,
469
+ phone: <Path d="M5 4h3.5l1.7 4.3-2.2 1.4a11 11 0 0 0 6.3 6.3l1.4-2.2L20 15.5V19a2 2 0 0 1-2.2 2A16.5 16.5 0 0 1 3 6.2 2 2 0 0 1 5 4z" {...p} />,
470
+ wifi: (
471
+ <>
472
+ <Path d="M2.5 9a14 14 0 0 1 19 0M5.8 12.5a9.2 9.2 0 0 1 12.4 0M9.1 16a4.5 4.5 0 0 1 5.8 0" {...p} />
473
+ <Circle cx={12} cy={19.2} r={1.1} fill={color} />
474
+ </>
475
+ ),
476
+ };
477
+ return (
478
+ <Svg width={size} height={size} viewBox="0 0 24 24">
479
+ {paths[name]}
480
+ </Svg>
481
+ );
482
+ }
483
+
484
+ const styles = StyleSheet.create({
485
+ row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: PAD_X, gap: GAP, overflow: 'hidden' },
486
+ disabled: { opacity: 0.45 },
487
+ leading: { alignItems: 'center', justifyContent: 'center' },
488
+ tile: { alignItems: 'center', justifyContent: 'center' },
489
+ body: { flex: 1, minWidth: 0, justifyContent: 'center' },
490
+ titleRow: { flexDirection: 'row', alignItems: 'baseline', gap: 8 },
491
+ title: { flex: 1, fontWeight: '400', letterSpacing: -0.2 },
492
+ meta: { fontSize: 12.5, fontVariant: ['tabular-nums'] },
493
+ sub: { marginTop: 2 },
494
+ desc: { marginTop: 2 },
495
+ trailing: { flexDirection: 'row', alignItems: 'center', gap: 8, flexShrink: 0, maxWidth: '50%' },
496
+ value: { fontSize: 15, flexShrink: 1 },
497
+ dot: { width: 9, height: 9, borderRadius: 5 },
498
+ badge: { minWidth: 22, height: 22, paddingHorizontal: 7, alignItems: 'center', justifyContent: 'center' },
499
+ badgeText: { fontSize: 12, fontWeight: '400', fontVariant: ['tabular-nums'] },
500
+ divider: { position: 'absolute', right: 0, bottom: 0, height: StyleSheet.hairlineWidth },
501
+ avatar: { alignItems: 'center', justifyContent: 'center', overflow: 'hidden' },
502
+ online: { position: 'absolute', right: -1, bottom: -1, borderWidth: 2 },
503
+ group: { gap: 8, alignSelf: 'stretch' },
504
+ header: { fontSize: 12.5, fontWeight: '400', textTransform: 'uppercase', letterSpacing: 0.6, paddingHorizontal: 16 },
505
+ card: { overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth },
506
+ footer: { fontSize: 12.5, lineHeight: 17, paddingHorizontal: 16 },
507
+ sectionHeader: { fontSize: 13, fontWeight: '400', letterSpacing: 0.3, paddingHorizontal: 16, paddingTop: 12, paddingBottom: 4 },
508
+ });