@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.
- package/LICENSE +21 -0
- package/README.md +32 -2
- package/bin/expo-kit.js +1204 -0
- package/bin/lib.mjs +343 -0
- package/bin/mcp.mjs +338 -0
- package/package.json +44 -4
- package/registry/_docs/accordion.json +99 -0
- package/registry/_docs/action-sheet.json +110 -0
- package/registry/_docs/alert.json +127 -0
- package/registry/_docs/avatar.json +203 -0
- package/registry/_docs/badge.json +117 -0
- package/registry/_docs/bottom-nav.json +119 -0
- package/registry/_docs/bottom-sheet.json +158 -0
- package/registry/_docs/bullet.json +24 -0
- package/registry/_docs/button-3d.json +122 -0
- package/registry/_docs/button.json +115 -0
- package/registry/_docs/calendar.json +182 -0
- package/registry/_docs/card-stack.json +108 -0
- package/registry/_docs/card.json +111 -0
- package/registry/_docs/carousel.json +102 -0
- package/registry/_docs/checkbox.json +93 -0
- package/registry/_docs/checkout.json +102 -0
- package/registry/_docs/chip-group.json +116 -0
- package/registry/_docs/color-swatch.json +66 -0
- package/registry/_docs/confetti.json +82 -0
- package/registry/_docs/coupon.json +176 -0
- package/registry/_docs/cube-pager.json +84 -0
- package/registry/_docs/dashboard.json +120 -0
- package/registry/_docs/date-picker.json +249 -0
- package/registry/_docs/dialog.json +162 -0
- package/registry/_docs/drawer.json +171 -0
- package/registry/_docs/fab.json +120 -0
- package/registry/_docs/flip-card.json +111 -0
- package/registry/_docs/floating-input.json +191 -0
- package/registry/_docs/gift-card.json +108 -0
- package/registry/_docs/glass-button.json +118 -0
- package/registry/_docs/glass-nav-bar.json +78 -0
- package/registry/_docs/glass-search-bar.json +90 -0
- package/registry/_docs/glass-segmented.json +66 -0
- package/registry/_docs/glass-sheet.json +108 -0
- package/registry/_docs/glass-tab-bar.json +76 -0
- package/registry/_docs/glass.json +78 -0
- package/registry/_docs/inbox.json +72 -0
- package/registry/_docs/isometric-bar.json +72 -0
- package/registry/_docs/kit.json +11 -0
- package/registry/_docs/kpi-card.json +11 -0
- package/registry/_docs/like-button.json +72 -0
- package/registry/_docs/liquidglass-tab.json +27 -0
- package/registry/_docs/list-item.json +200 -0
- package/registry/_docs/loyalty-card.json +108 -0
- package/registry/_docs/morph-button.json +108 -0
- package/registry/_docs/number-ticker.json +96 -0
- package/registry/_docs/offers.json +78 -0
- package/registry/_docs/onboarding.json +66 -0
- package/registry/_docs/otp-input.json +134 -0
- package/registry/_docs/pagination-dots.json +88 -0
- package/registry/_docs/parallax-layers.json +84 -0
- package/registry/_docs/parallax-scroll.json +186 -0
- package/registry/_docs/password-input.json +92 -0
- package/registry/_docs/payment-card.json +153 -0
- package/registry/_docs/pixel-bar.json +66 -0
- package/registry/_docs/polar-area.json +78 -0
- package/registry/_docs/profile.json +84 -0
- package/registry/_docs/progress-steps.json +92 -0
- package/registry/_docs/progress.json +132 -0
- package/registry/_docs/radio-group.json +92 -0
- package/registry/_docs/rating.json +90 -0
- package/registry/_docs/ring-carousel.json +102 -0
- package/registry/_docs/scratch-card.json +90 -0
- package/registry/_docs/search-bar.json +140 -0
- package/registry/_docs/search-results.json +120 -0
- package/registry/_docs/segmented-control.json +103 -0
- package/registry/_docs/select.json +252 -0
- package/registry/_docs/settings.json +72 -0
- package/registry/_docs/shimmer-button.json +78 -0
- package/registry/_docs/sign-in.json +198 -0
- package/registry/_docs/skeleton.json +142 -0
- package/registry/_docs/slide-to-confirm.json +84 -0
- package/registry/_docs/slider.json +157 -0
- package/registry/_docs/spin-wheel.json +114 -0
- package/registry/_docs/spinner.json +91 -0
- package/registry/_docs/stepper.json +168 -0
- package/registry/_docs/swipe-deck.json +102 -0
- package/registry/_docs/swipeable-row.json +72 -0
- package/registry/_docs/switch.json +104 -0
- package/registry/_docs/tabs.json +105 -0
- package/registry/_docs/tag-input.json +128 -0
- package/registry/_docs/textarea.json +175 -0
- package/registry/_docs/ticket-card.json +174 -0
- package/registry/_docs/ticket-shape.json +11 -0
- package/registry/_docs/tilt-card.json +141 -0
- package/registry/_docs/time-picker.json +253 -0
- package/registry/_docs/timeline-chart.json +30 -0
- package/registry/_docs/timeline.json +156 -0
- package/registry/_docs/toast.json +161 -0
- package/registry/_docs/treemap.json +30 -0
- package/registry/_docs/waterfall.json +36 -0
- package/registry/_docs/wheel-list.json +102 -0
- package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
- package/registry/accordion/files/src/lib/kit.tsx +219 -0
- package/registry/accordion.json +17 -0
- package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
- package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/action-sheet.json +18 -0
- package/registry/alert/files/src/components/ui/alert.tsx +654 -0
- package/registry/alert/files/src/components/ui/button.tsx +274 -0
- package/registry/alert/files/src/lib/kit.tsx +219 -0
- package/registry/alert.json +20 -0
- package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
- package/registry/avatar/files/src/lib/kit.tsx +219 -0
- package/registry/avatar.json +18 -0
- package/registry/badge/files/src/components/ui/badge.tsx +316 -0
- package/registry/badge/files/src/lib/kit.tsx +219 -0
- package/registry/badge.json +17 -0
- package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
- package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-nav.json +18 -0
- package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
- package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
- package/registry/bottom-sheet.json +17 -0
- package/registry/bullet.json +14 -0
- package/registry/button/files/src/components/ui/button.tsx +274 -0
- package/registry/button/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
- package/registry/button-3d/files/src/lib/kit.tsx +219 -0
- package/registry/button-3d.json +18 -0
- package/registry/button.json +18 -0
- package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
- package/registry/calendar/files/src/lib/kit.tsx +219 -0
- package/registry/calendar.json +17 -0
- package/registry/card/files/src/components/ui/card.tsx +300 -0
- package/registry/card/files/src/lib/kit.tsx +219 -0
- package/registry/card-stack.json +15 -0
- package/registry/card.json +17 -0
- package/registry/carousel.json +15 -0
- package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
- package/registry/checkbox/files/src/lib/kit.tsx +219 -0
- package/registry/checkbox.json +17 -0
- package/registry/checkout.json +26 -0
- package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
- package/registry/chip-group/files/src/lib/kit.tsx +219 -0
- package/registry/chip-group.json +17 -0
- package/registry/color-swatch.json +17 -0
- package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
- package/registry/confetti/files/src/lib/kit.tsx +219 -0
- package/registry/confetti.json +15 -0
- package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
- package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/coupon/files/src/lib/kit.tsx +219 -0
- package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/coupon.json +23 -0
- package/registry/cube-pager.json +15 -0
- package/registry/dashboard.json +32 -0
- package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
- package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
- package/registry/date-picker/files/src/lib/kit.tsx +219 -0
- package/registry/date-picker.json +19 -0
- package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
- package/registry/dialog/files/src/lib/kit.tsx +219 -0
- package/registry/dialog.json +18 -0
- package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
- package/registry/drawer/files/src/lib/kit.tsx +219 -0
- package/registry/drawer.json +18 -0
- package/registry/fab.json +18 -0
- package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/flip-card/files/src/lib/kit.tsx +219 -0
- package/registry/flip-card.json +15 -0
- package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/floating-input/files/src/lib/kit.tsx +219 -0
- package/registry/floating-input.json +17 -0
- package/registry/gift-card.json +21 -0
- package/registry/glass-button.json +22 -0
- package/registry/glass-nav-bar.json +21 -0
- package/registry/glass-search-bar.json +21 -0
- package/registry/glass-segmented.json +22 -0
- package/registry/glass-sheet.json +22 -0
- package/registry/glass-tab-bar.json +22 -0
- package/registry/glass.json +19 -0
- package/registry/inbox.json +30 -0
- package/registry/isometric-bar.json +14 -0
- package/registry/kit/files/src/lib/kit.tsx +219 -0
- package/registry/kit.json +11 -0
- package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
- package/registry/kpi-card.json +14 -0
- package/registry/like-button.json +18 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
- package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
- package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
- package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
- package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
- package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
- package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
- package/registry/liquidglass-tab.json +23 -0
- package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
- package/registry/list-item/files/src/lib/kit.tsx +219 -0
- package/registry/list-item.json +17 -0
- package/registry/loyalty-card.json +23 -0
- package/registry/morph-button.json +18 -0
- package/registry/number-ticker.json +15 -0
- package/registry/offers.json +24 -0
- package/registry/onboarding.json +17 -0
- package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
- package/registry/otp-input/files/src/lib/kit.tsx +219 -0
- package/registry/otp-input.json +17 -0
- package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
- package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
- package/registry/pagination-dots.json +15 -0
- package/registry/parallax-layers.json +15 -0
- package/registry/parallax-scroll.json +17 -0
- package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
- package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
- package/registry/password-input/files/src/lib/kit.tsx +219 -0
- package/registry/password-input.json +19 -0
- package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
- package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
- package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/payment-card/files/src/lib/kit.tsx +219 -0
- package/registry/payment-card.json +21 -0
- package/registry/pixel-bar.json +14 -0
- package/registry/polar-area.json +14 -0
- package/registry/profile.json +17 -0
- package/registry/progress/files/src/components/ui/progress.tsx +883 -0
- package/registry/progress/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
- package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
- package/registry/progress-steps.json +18 -0
- package/registry/progress.json +17 -0
- package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
- package/registry/radio-group/files/src/lib/kit.tsx +219 -0
- package/registry/radio-group.json +18 -0
- package/registry/rating.json +18 -0
- package/registry/ring-carousel.json +15 -0
- package/registry/scratch-card.json +18 -0
- package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
- package/registry/search-bar/files/src/lib/kit.tsx +219 -0
- package/registry/search-bar.json +17 -0
- package/registry/search-results.json +30 -0
- package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
- package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
- package/registry/segmented-control.json +18 -0
- package/registry/select/files/src/components/ui/select.tsx +1398 -0
- package/registry/select/files/src/lib/kit.tsx +219 -0
- package/registry/select.json +17 -0
- package/registry/settings.json +26 -0
- package/registry/shimmer-button.json +17 -0
- package/registry/sign-in.json +26 -0
- package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
- package/registry/skeleton/files/src/lib/kit.tsx +219 -0
- package/registry/skeleton.json +17 -0
- package/registry/slide-to-confirm.json +18 -0
- package/registry/slider/files/src/components/ui/slider.tsx +716 -0
- package/registry/slider/files/src/lib/kit.tsx +219 -0
- package/registry/slider.json +17 -0
- package/registry/spin-wheel.json +18 -0
- package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
- package/registry/spinner/files/src/lib/kit.tsx +219 -0
- package/registry/spinner.json +17 -0
- package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
- package/registry/stepper/files/src/lib/kit.tsx +219 -0
- package/registry/stepper.json +17 -0
- package/registry/swipe-deck.json +15 -0
- package/registry/swipeable-row.json +15 -0
- package/registry/switch/files/src/components/ui/switch.tsx +344 -0
- package/registry/switch/files/src/lib/kit.tsx +219 -0
- package/registry/switch.json +18 -0
- package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
- package/registry/tabs/files/src/lib/kit.tsx +219 -0
- package/registry/tabs.json +17 -0
- package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
- package/registry/tag-input/files/src/lib/kit.tsx +219 -0
- package/registry/tag-input.json +17 -0
- package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
- package/registry/textarea/files/src/lib/kit.tsx +219 -0
- package/registry/textarea.json +17 -0
- package/registry/ticket-card.json +24 -0
- package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
- package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
- package/registry/ticket-shape.json +17 -0
- package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
- package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
- package/registry/tilt-card.json +17 -0
- package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
- package/registry/time-picker/files/src/lib/kit.tsx +219 -0
- package/registry/time-picker.json +17 -0
- package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
- package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
- package/registry/timeline/files/src/lib/kit.tsx +219 -0
- package/registry/timeline-chart.json +14 -0
- package/registry/timeline.json +20 -0
- package/registry/toast/files/src/components/ui/button.tsx +274 -0
- package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
- package/registry/toast/files/src/lib/kit.tsx +219 -0
- package/registry/toast.json +20 -0
- package/registry/treemap.json +14 -0
- package/registry/waterfall.json +14 -0
- package/registry/wheel-list.json +17 -0
- 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,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
|
+
});
|