@expokit/cli 0.0.0-stage → 0.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "kit",
3
+ "type": "lib",
4
+ "title": "Kit theme",
5
+ "description": "Design tokens, KitProvider and motion presets shared by every ExpoKit component.",
6
+ "tier": "free",
7
+ "dependencies": [],
8
+ "registryDependencies": [],
9
+ "sharedFiles": [],
10
+ "filesDir": "kit/files"
11
+ }
@@ -0,0 +1,45 @@
1
+ import { StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
2
+ import { Sparkline, useChartTheme } from "expo-charts";
3
+
4
+ export interface KpiCardProps {
5
+ label: string;
6
+ value: string;
7
+ /** Percentage change, e.g. 12.4 or -3.1. */
8
+ delta?: number;
9
+ /** Recent values for the sparkline (oldest first). */
10
+ trend?: number[];
11
+ style?: StyleProp<ViewStyle>;
12
+ }
13
+
14
+ /** Big number + trend badge + sparkline. Colours follow <ChartProvider>. */
15
+ export function KpiCard({ label, value, delta, trend, style }: KpiCardProps) {
16
+ const theme = useChartTheme();
17
+ const up = (delta ?? 0) >= 0;
18
+ const tone = up ? theme.positive : theme.negative;
19
+ return (
20
+ <View style={[styles.card, { backgroundColor: theme.card, borderColor: theme.border }, style]}>
21
+ <View style={styles.row}>
22
+ <Text style={[styles.label, { color: theme.mutedForeground }]}>{label}</Text>
23
+ {delta !== undefined ? (
24
+ <View style={[styles.badge, { borderColor: tone }]}>
25
+ <Text style={[styles.badgeText, { color: tone }]}>
26
+ {up ? "+" : ""}
27
+ {delta.toFixed(1)}%
28
+ </Text>
29
+ </View>
30
+ ) : null}
31
+ </View>
32
+ <Text style={[styles.value, { color: theme.foreground }]}>{value}</Text>
33
+ {trend && trend.length > 1 ? <Sparkline data={trend} height={40} color={tone} /> : null}
34
+ </View>
35
+ );
36
+ }
37
+
38
+ const styles = StyleSheet.create({
39
+ card: { borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, padding: 16, gap: 6 },
40
+ row: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" },
41
+ label: { fontSize: 13, fontWeight: "400" },
42
+ badge: { borderWidth: StyleSheet.hairlineWidth, borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2 },
43
+ badgeText: { fontSize: 11, fontWeight: "400", fontVariant: ["tabular-nums"] },
44
+ value: { fontSize: 28, fontWeight: "400", letterSpacing: -0.5, fontVariant: ["tabular-nums"] },
45
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "kpi-card",
3
+ "type": "chart",
4
+ "title": "KPI card",
5
+ "description": "Big number, trend badge and an animated sparkline. Built on expo-charts.",
6
+ "tier": "free",
7
+ "dependencies": [
8
+ "expo-charts",
9
+ "react-native-svg"
10
+ ],
11
+ "registryDependencies": [],
12
+ "sharedFiles": [],
13
+ "filesDir": "kpi-card/files"
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "like-button",
3
+ "type": "component",
4
+ "title": "Like Button",
5
+ "description": "Heart, star or bookmark that pops, throws a ring and a burst of coloured particles when liked, with an optional counter that rolls to the new number.",
6
+ "tier": "pro",
7
+ "dependencies": [
8
+ "react-native-svg",
9
+ "expo-haptics"
10
+ ],
11
+ "registryDependencies": [
12
+ "kit"
13
+ ],
14
+ "sharedFiles": [
15
+ "lib/kit.tsx"
16
+ ],
17
+ "filesDir": null
18
+ }
@@ -0,0 +1,97 @@
1
+ import React from 'react';
2
+ import { DynamicColorIOS, Platform } from 'react-native';
3
+ import { Tabs } from 'expo-router/js-tabs';
4
+ // SDK 54–57 path. On SDK 58+ change this to 'expo-router/native-tabs'.
5
+ import { NativeTabs } from 'expo-router/unstable-native-tabs';
6
+
7
+ import { GlassTabBar } from '@/components/GlassTabBar';
8
+ import { USE_NATIVE_TABS } from '@/lib/tab-bar';
9
+ import {
10
+ asFamily,
11
+ resolveIconLibrary,
12
+ resolveIconNames,
13
+ TAB_BAR,
14
+ TABS,
15
+ type TabItem,
16
+ } from '@/lib/tabs';
17
+
18
+ type IconProps = React.ComponentProps<typeof NativeTabs.Trigger.Icon>;
19
+
20
+ function dynamicColor(pair: { light: string; dark: string }) {
21
+ return Platform.OS === 'ios' ? DynamicColorIOS(pair) : pair.light;
22
+ }
23
+
24
+ /** Vector-font icon rasterised for the native bar (Android, or iOS when no SF Symbol is set). */
25
+ function vectorSrc(tab: TabItem) {
26
+ const library = resolveIconLibrary(tab);
27
+ const { name, selected } = resolveIconNames(tab);
28
+ if (!library || !name) return undefined;
29
+
30
+ const family = asFamily(library);
31
+ const icon = <NativeTabs.Trigger.VectorIcon family={family} name={name} />;
32
+ if (!selected) return icon;
33
+ return { default: icon, selected: <NativeTabs.Trigger.VectorIcon family={family} name={selected} /> };
34
+ }
35
+
36
+ /**
37
+ * Returns the Icon element itself (not a wrapper component): NativeTabs reads its
38
+ * children by type, so <NativeTabs.Trigger.Icon> must be a direct child of the Trigger.
39
+ */
40
+ function nativeIcon(tab: TabItem, tint: IconProps['selectedColor']) {
41
+ const src = tab.src ?? vectorSrc(tab);
42
+ if (!tab.sf && !src) return null;
43
+ // sf wins on iOS, src is used on Android.
44
+ const props = { sf: tab.sf, src, selectedColor: tint } as IconProps;
45
+ return <NativeTabs.Trigger.Icon {...props} />;
46
+ }
47
+
48
+ function NativeTabLayout() {
49
+ const tint = dynamicColor(TAB_BAR.colors.tint);
50
+ const inactive = dynamicColor(TAB_BAR.colors.inactive);
51
+ const { fontSize, fontWeight, selectedFontWeight } = TAB_BAR.labelStyle;
52
+
53
+ return (
54
+ <NativeTabs
55
+ tintColor={tint}
56
+ iconColor={inactive}
57
+ labelVisibilityMode={TAB_BAR.labelVisibilityMode}
58
+ minimizeBehavior={TAB_BAR.minimizeBehavior}
59
+ labelStyle={{ color: inactive, fontSize, fontWeight }}
60
+ {...(TAB_BAR.backgroundColor != null ? { backgroundColor: TAB_BAR.backgroundColor } : null)}
61
+ >
62
+ {TABS.map((tab) => (
63
+ <NativeTabs.Trigger key={tab.name} name={tab.name} hidden={tab.hidden}>
64
+ {nativeIcon(tab, tint)}
65
+ <NativeTabs.Trigger.Label
66
+ hidden={tab.hiddenLabel}
67
+ selectedStyle={{ color: tint, fontSize, fontWeight: selectedFontWeight }}
68
+ >
69
+ {tab.title}
70
+ </NativeTabs.Trigger.Label>
71
+ {tab.badge ? <NativeTabs.Trigger.Badge>{tab.badge}</NativeTabs.Trigger.Badge> : null}
72
+ </NativeTabs.Trigger>
73
+ ))}
74
+ </NativeTabs>
75
+ );
76
+ }
77
+
78
+ function GlassTabLayout() {
79
+ return (
80
+ <Tabs
81
+ tabBar={(props) => <GlassTabBar {...props} />}
82
+ screenOptions={{ headerShown: false, animation: 'fade' }}
83
+ >
84
+ {TABS.map((tab) => (
85
+ <Tabs.Screen
86
+ key={tab.name}
87
+ name={tab.name}
88
+ options={{ title: tab.title, href: tab.hidden ? null : undefined }}
89
+ />
90
+ ))}
91
+ </Tabs>
92
+ );
93
+ }
94
+
95
+ export default function TabLayout() {
96
+ return USE_NATIVE_TABS ? <NativeTabLayout /> : <GlassTabLayout />;
97
+ }
@@ -0,0 +1,5 @@
1
+ import { TabScreenFromConfig } from '@/components/TabScreen';
2
+
3
+ export default function AlertsScreen() {
4
+ return <TabScreenFromConfig tabName="alerts" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { TabScreenFromConfig } from '@/components/TabScreen';
2
+
3
+ export default function ExploreScreen() {
4
+ return <TabScreenFromConfig tabName="explore" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { TabScreenFromConfig } from '@/components/TabScreen';
2
+
3
+ export default function HomeScreen() {
4
+ return <TabScreenFromConfig tabName="index" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { TabScreenFromConfig } from '@/components/TabScreen';
2
+
3
+ export default function ProfileScreen() {
4
+ return <TabScreenFromConfig tabName="profile" />;
5
+ }
@@ -0,0 +1,33 @@
1
+ import 'react-native-gesture-handler';
2
+ import React, { useEffect } from 'react';
3
+ import { StyleSheet, useColorScheme } from 'react-native';
4
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
5
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
6
+ // SDK 56+: navigation themes come from expo-router itself. Using @react-navigation/native's
7
+ // ThemeProvider here does nothing (expo-router ships its own copy) and causes a white flash.
8
+ import { DarkTheme, DefaultTheme, Slot, ThemeProvider } from 'expo-router';
9
+ import * as SplashScreen from 'expo-splash-screen';
10
+
11
+ SplashScreen.preventAutoHideAsync();
12
+
13
+ export default function RootLayout() {
14
+ const isDark = useColorScheme() === 'dark';
15
+
16
+ useEffect(() => {
17
+ SplashScreen.hideAsync();
18
+ }, []);
19
+
20
+ return (
21
+ <GestureHandlerRootView style={styles.root}>
22
+ <SafeAreaProvider>
23
+ <ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
24
+ <Slot />
25
+ </ThemeProvider>
26
+ </SafeAreaProvider>
27
+ </GestureHandlerRootView>
28
+ );
29
+ }
30
+
31
+ const styles = StyleSheet.create({
32
+ root: { flex: 1 },
33
+ });
@@ -0,0 +1,259 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Image,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ useColorScheme,
11
+ useWindowDimensions,
12
+ type ColorValue,
13
+ type ImageSourcePropType,
14
+ } from 'react-native';
15
+ import { BlurView } from 'expo-blur';
16
+ import * as Haptics from 'expo-haptics';
17
+ import type { BottomTabBarProps } from 'expo-router/js-tabs';
18
+
19
+ import { GLASS_BAR_HEIGHT, setTabBarCollapsed, tabBarCollapse } from '@/lib/tab-bar';
20
+ import { getTab, resolveIconLibrary, resolveIconNames, TAB_BAR, type TabItem } from '@/lib/tabs';
21
+
22
+ /**
23
+ * Liquid-Glass look-alike for devices without the real thing (iOS 18 and older, Android, web).
24
+ * A floating blur pill with a sliding selection bubble. On scroll it collapses into a single
25
+ * glass circle holding the active tab — tap the circle to expand it again, like iOS 26.
26
+ */
27
+ export function GlassTabBar({ state, navigation, insets }: BottomTabBarProps) {
28
+ const isDark = useColorScheme() === 'dark';
29
+ const { width: screenW } = useWindowDimensions();
30
+ const tint = isDark ? TAB_BAR.colors.tint.dark : TAB_BAR.colors.tint.light;
31
+ const inactive = isDark ? TAB_BAR.colors.inactive.dark : TAB_BAR.colors.inactive.light;
32
+
33
+ const routes = state.routes.filter((r) => !getTab(r.name)?.hidden);
34
+ const activeKey = state.routes[state.index]?.key;
35
+ const activeIndex = Math.max(0, routes.findIndex((r) => r.key === activeKey));
36
+ const activeTab = getTab(routes[activeIndex]?.name ?? '');
37
+
38
+ const fullW = Math.min(screenW - 32, 520);
39
+ const itemW = fullW / Math.max(routes.length, 1);
40
+ const bubbleX = useRef(new Animated.Value(activeIndex * itemW)).current;
41
+ const [collapsed, setCollapsed] = useState(false);
42
+
43
+ useEffect(() => {
44
+ Animated.spring(bubbleX, {
45
+ toValue: activeIndex * itemW,
46
+ useNativeDriver: true,
47
+ damping: 18,
48
+ stiffness: 240,
49
+ mass: 0.8,
50
+ }).start();
51
+ setTabBarCollapsed(false);
52
+ }, [activeIndex, itemW, bubbleX]);
53
+
54
+ useEffect(() => {
55
+ const id = tabBarCollapse.addListener(({ value }) => setCollapsed(value > 0.5));
56
+ return () => tabBarCollapse.removeListener(id);
57
+ }, []);
58
+
59
+ const width = tabBarCollapse.interpolate({ inputRange: [0, 1], outputRange: [fullW, GLASS_BAR_HEIGHT] });
60
+ const rowOpacity = tabBarCollapse.interpolate({ inputRange: [0, 0.45], outputRange: [1, 0], extrapolate: 'clamp' });
61
+ const dotOpacity = tabBarCollapse.interpolate({ inputRange: [0.55, 1], outputRange: [0, 1], extrapolate: 'clamp' });
62
+ const dotScale = tabBarCollapse.interpolate({ inputRange: [0.55, 1], outputRange: [0.6, 1], extrapolate: 'clamp' });
63
+
64
+ return (
65
+ <View pointerEvents="box-none" style={[styles.host, { bottom: Math.max(insets.bottom, 12) }]}>
66
+ <Animated.View style={[styles.shadow, { width }]}>
67
+ <View style={[styles.glass, { borderColor: isDark ? 'rgba(255,255,255,0.14)' : 'rgba(255,255,255,0.7)' }]}>
68
+ <BlurView
69
+ intensity={Platform.OS === 'ios' ? 70 : 40}
70
+ tint={isDark ? 'dark' : 'light'}
71
+ style={StyleSheet.absoluteFill}
72
+ />
73
+ <View
74
+ style={[
75
+ StyleSheet.absoluteFill,
76
+ { backgroundColor: isDark ? 'rgba(28,28,34,0.55)' : 'rgba(255,255,255,0.55)' },
77
+ ]}
78
+ />
79
+ {/* Specular rim along the top edge. */}
80
+ <View style={[styles.rim, { backgroundColor: isDark ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.95)' }]} />
81
+
82
+ <Animated.View
83
+ pointerEvents={collapsed ? 'none' : 'auto'}
84
+ style={[styles.row, { width: fullW, opacity: rowOpacity }]}
85
+ >
86
+ <Animated.View
87
+ style={[
88
+ styles.bubble,
89
+ {
90
+ width: itemW - 8,
91
+ backgroundColor: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.06)',
92
+ transform: [{ translateX: bubbleX }],
93
+ },
94
+ ]}
95
+ />
96
+ {routes.map((route) => {
97
+ const tab = getTab(route.name);
98
+ const focused = route.key === activeKey;
99
+ const color = focused ? tint : inactive;
100
+ const onPress = () => {
101
+ const event = navigation.emit({ type: 'tabPress', target: route.key, canPreventDefault: true });
102
+ if (!focused && !event.defaultPrevented) {
103
+ Haptics.selectionAsync().catch(() => {});
104
+ navigation.navigate(route.name, route.params);
105
+ }
106
+ };
107
+ return (
108
+ <TabButton
109
+ key={route.key}
110
+ tab={tab}
111
+ title={tab?.title ?? route.name}
112
+ focused={focused}
113
+ color={color}
114
+ width={itemW}
115
+ onPress={onPress}
116
+ />
117
+ );
118
+ })}
119
+ </Animated.View>
120
+
121
+ <Animated.View
122
+ pointerEvents={collapsed ? 'auto' : 'none'}
123
+ style={[styles.dot, { opacity: dotOpacity, transform: [{ scale: dotScale }] }]}
124
+ >
125
+ <Pressable
126
+ accessibilityRole="button"
127
+ accessibilityLabel="Show tabs"
128
+ onPress={() => {
129
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
130
+ setTabBarCollapsed(false);
131
+ }}
132
+ style={styles.dotPress}
133
+ >
134
+ <TabIcon tab={activeTab} focused color={tint} size={26} />
135
+ </Pressable>
136
+ </Animated.View>
137
+ </View>
138
+ </Animated.View>
139
+ </View>
140
+ );
141
+ }
142
+
143
+ function TabButton({
144
+ tab,
145
+ title,
146
+ focused,
147
+ color,
148
+ width,
149
+ onPress,
150
+ }: {
151
+ tab?: TabItem;
152
+ title: string;
153
+ focused: boolean;
154
+ color: ColorValue;
155
+ width: number;
156
+ onPress: () => void;
157
+ }) {
158
+ const press = useRef(new Animated.Value(1)).current;
159
+ const to = (v: number) =>
160
+ Animated.spring(press, { toValue: v, useNativeDriver: true, speed: 40, bounciness: 10 }).start();
161
+
162
+ return (
163
+ <Pressable
164
+ accessibilityRole="tab"
165
+ accessibilityState={{ selected: focused }}
166
+ accessibilityLabel={title}
167
+ onPress={onPress}
168
+ onPressIn={() => to(0.86)}
169
+ onPressOut={() => to(1)}
170
+ style={[styles.item, { width }]}
171
+ >
172
+ <Animated.View style={{ alignItems: 'center', transform: [{ scale: press }] }}>
173
+ <View>
174
+ <TabIcon tab={tab} focused={focused} color={color} size={23} />
175
+ {tab?.badge ? (
176
+ <View style={styles.badge}>
177
+ <Text style={styles.badgeText}>{tab.badge}</Text>
178
+ </View>
179
+ ) : null}
180
+ </View>
181
+ {tab?.hiddenLabel ||
182
+ TAB_BAR.labelVisibilityMode === 'unlabeled' ||
183
+ (TAB_BAR.labelVisibilityMode === 'selected' && !focused) ? null : (
184
+ <Text
185
+ numberOfLines={1}
186
+ style={[
187
+ styles.label,
188
+ {
189
+ color,
190
+ fontSize: TAB_BAR.labelStyle.fontSize,
191
+ fontWeight: focused ? TAB_BAR.labelStyle.selectedFontWeight : TAB_BAR.labelStyle.fontWeight,
192
+ },
193
+ ]}
194
+ >
195
+ {title}
196
+ </Text>
197
+ )}
198
+ </Animated.View>
199
+ </Pressable>
200
+ );
201
+ }
202
+
203
+ type IconComponent = React.ComponentType<{ name: string; size: number; color: ColorValue }>;
204
+
205
+ /** Same icon config as the native bar: `src` image, or a vector-icon set / custom icon font. */
206
+ function TabIcon({ tab, focused, color, size }: { tab?: TabItem; focused: boolean; color: ColorValue; size: number }) {
207
+ if (tab?.src) {
208
+ const src = tab.src as ImageSourcePropType | { default: ImageSourcePropType; selected: ImageSourcePropType };
209
+ const pair = typeof src === 'object' && src !== null && 'selected' in src ? src : null;
210
+ const source = pair ? (focused ? pair.selected : pair.default) : (src as ImageSourcePropType);
211
+ return <Image source={source} style={{ width: size, height: size, tintColor: color as string }} />;
212
+ }
213
+ const library = tab ? resolveIconLibrary(tab) : undefined;
214
+ const { name, selected } = tab ? resolveIconNames(tab) : {};
215
+ if (typeof library === 'function' && name) {
216
+ const Icon = library as IconComponent;
217
+ return <Icon name={focused && selected ? selected : name} size={size} color={color} />;
218
+ }
219
+ return <View style={{ width: size * 0.4, height: size * 0.4, borderRadius: size, backgroundColor: color, margin: size * 0.3 }} />;
220
+ }
221
+
222
+ const styles = StyleSheet.create({
223
+ host: { position: 'absolute', left: 16, right: 16, alignItems: 'flex-start' },
224
+ shadow: {
225
+ height: GLASS_BAR_HEIGHT,
226
+ borderRadius: GLASS_BAR_HEIGHT / 2,
227
+ shadowColor: '#000',
228
+ shadowOpacity: 0.18,
229
+ shadowRadius: 22,
230
+ shadowOffset: { width: 0, height: 10 },
231
+ elevation: 12,
232
+ },
233
+ glass: {
234
+ flex: 1,
235
+ borderRadius: GLASS_BAR_HEIGHT / 2,
236
+ overflow: 'hidden',
237
+ borderWidth: StyleSheet.hairlineWidth,
238
+ },
239
+ rim: { position: 'absolute', top: 0, left: 24, right: 24, height: StyleSheet.hairlineWidth * 2, borderRadius: 1 },
240
+ row: { position: 'absolute', top: 0, bottom: 0, left: 0, flexDirection: 'row', alignItems: 'center' },
241
+ bubble: { position: 'absolute', top: 5, bottom: 5, left: 4, borderRadius: (GLASS_BAR_HEIGHT - 10) / 2 },
242
+ item: { height: GLASS_BAR_HEIGHT, alignItems: 'center', justifyContent: 'center' },
243
+ label: { marginTop: 3, letterSpacing: 0.1 },
244
+ badge: {
245
+ position: 'absolute',
246
+ top: -4,
247
+ right: -10,
248
+ minWidth: 17,
249
+ height: 17,
250
+ paddingHorizontal: 4,
251
+ borderRadius: 9,
252
+ backgroundColor: '#FF3B30',
253
+ alignItems: 'center',
254
+ justifyContent: 'center',
255
+ },
256
+ badgeText: { color: '#fff', fontSize: 10, fontWeight: '400' },
257
+ dot: { position: 'absolute', top: 0, left: 0, width: GLASS_BAR_HEIGHT, height: GLASS_BAR_HEIGHT },
258
+ dotPress: { flex: 1, alignItems: 'center', justifyContent: 'center' },
259
+ });
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import {
3
+ ScrollView,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ useColorScheme,
8
+ } from 'react-native';
9
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
10
+
11
+ import { useTabBarInset, useTabBarScroll, USE_NATIVE_TABS } from '@/lib/tab-bar';
12
+ import { getTab } from '@/lib/tabs';
13
+
14
+ interface TabScreenProps {
15
+ title: string;
16
+ description: string;
17
+ accent: string;
18
+ }
19
+
20
+ export function TabScreen({ title, description, accent }: TabScreenProps) {
21
+ const isDark = useColorScheme() === 'dark';
22
+ const insets = useSafeAreaInsets();
23
+ const onScroll = useTabBarScroll();
24
+ const bottomInset = useTabBarInset();
25
+ const bg = isDark ? '#0f0f1a' : '#f2f2f7';
26
+ const text = isDark ? '#fff' : '#1c1c1e';
27
+ const sub = isDark ? '#9898a8' : '#6c6c70';
28
+
29
+ return (
30
+ <View collapsable={false} style={styles.root}>
31
+ <ScrollView
32
+ style={{ flex: 1, backgroundColor: bg }}
33
+ contentContainerStyle={[
34
+ styles.scroll,
35
+ { paddingTop: USE_NATIVE_TABS ? 12 : insets.top + 12, paddingBottom: bottomInset },
36
+ ]}
37
+ showsVerticalScrollIndicator={false}
38
+ // Native tabs inset content for the system bars; the glass fallback pads it manually.
39
+ contentInsetAdjustmentBehavior={USE_NATIVE_TABS ? 'automatic' : 'never'}
40
+ scrollEventThrottle={16}
41
+ onScroll={onScroll}
42
+ >
43
+ <View style={[styles.hero, { backgroundColor: accent }]}>
44
+ <Text style={styles.heroTitle}>{title}</Text>
45
+ <Text style={styles.heroHint}>
46
+ {USE_NATIVE_TABS ? 'Liquid Glass · scroll down to collapse' : 'Glass fallback · scroll down to collapse'}
47
+ </Text>
48
+ </View>
49
+ <Text style={[styles.title, { color: text }]}>{title}</Text>
50
+ <Text style={[styles.subtitle, { color: sub }]}>{description}</Text>
51
+
52
+ {SECTIONS.map((n) => (
53
+ <View
54
+ key={n}
55
+ style={[
56
+ styles.card,
57
+ { backgroundColor: isDark ? '#1c1c2e' : '#ffffff' },
58
+ ]}
59
+ >
60
+ <Text style={[styles.cardTitle, { color: text }]}>Section {n}</Text>
61
+ <Text style={[styles.cardSub, { color: sub }]}>
62
+ iOS 26+ gets the system Liquid Glass bar. Older iOS, Android and web get the
63
+ blur glass fallback — both merge into a glass circle when you scroll down.
64
+ </Text>
65
+ </View>
66
+ ))}
67
+ </ScrollView>
68
+ </View>
69
+ );
70
+ }
71
+
72
+ export function TabScreenFromConfig({ tabName }: { tabName: string }) {
73
+ const tab = getTab(tabName);
74
+ if (!tab) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <TabScreen
80
+ title={tab.title}
81
+ description={
82
+ tab.description ??
83
+ 'Scroll to test native tab bar minimize behavior on iOS 26+.'
84
+ }
85
+ accent={tab.accent ?? '#6366f1'}
86
+ />
87
+ );
88
+ }
89
+
90
+ const SECTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
91
+
92
+ const styles = StyleSheet.create({
93
+ root: { flex: 1 },
94
+ scroll: { paddingHorizontal: 20 },
95
+ hero: {
96
+ height: 140,
97
+ borderRadius: 20,
98
+ alignItems: 'center',
99
+ justifyContent: 'center',
100
+ marginBottom: 20,
101
+ },
102
+ heroTitle: {
103
+ color: '#fff',
104
+ fontSize: 28,
105
+ fontWeight: '400',
106
+ },
107
+ heroHint: {
108
+ color: 'rgba(255,255,255,0.75)',
109
+ fontSize: 13,
110
+ marginTop: 6,
111
+ fontWeight: '400',
112
+ },
113
+ title: { fontSize: 34, fontWeight: '400', letterSpacing: -0.5 },
114
+ subtitle: { fontSize: 15, marginTop: 8, marginBottom: 20, lineHeight: 22 },
115
+ card: {
116
+ borderRadius: 14,
117
+ padding: 16,
118
+ marginBottom: 10,
119
+ },
120
+ cardTitle: { fontSize: 17, fontWeight: '400' },
121
+ cardSub: { fontSize: 14, marginTop: 4, lineHeight: 20 },
122
+ });