lovdacn 0.0.1 → 0.0.3

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 (156) hide show
  1. package/README.md +72 -106
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +224 -0
  4. package/package.json +41 -63
  5. package/templates/nativewind/.vscode/extensions.json +1 -0
  6. package/templates/nativewind/.vscode/settings.json +7 -0
  7. package/templates/nativewind/AGENTS.md +3 -0
  8. package/templates/nativewind/CLAUDE.md +1 -0
  9. package/{LICENSE → templates/nativewind/LICENSE} +21 -21
  10. package/templates/nativewind/README.md +56 -0
  11. package/templates/nativewind/app.json +43 -0
  12. package/templates/nativewind/assets/expo.icon/Assets/expo-symbol 2.svg +3 -0
  13. package/templates/nativewind/assets/expo.icon/Assets/grid.png +0 -0
  14. package/templates/nativewind/assets/expo.icon/icon.json +40 -0
  15. package/templates/nativewind/assets/images/android-icon-background.png +0 -0
  16. package/templates/nativewind/assets/images/android-icon-foreground.png +0 -0
  17. package/templates/nativewind/assets/images/android-icon-monochrome.png +0 -0
  18. package/templates/nativewind/assets/images/expo-badge-white.png +0 -0
  19. package/templates/nativewind/assets/images/expo-badge.png +0 -0
  20. package/templates/nativewind/assets/images/expo-logo.png +0 -0
  21. package/templates/nativewind/assets/images/favicon.png +0 -0
  22. package/templates/nativewind/assets/images/icon.png +0 -0
  23. package/templates/nativewind/assets/images/logo-glow.png +0 -0
  24. package/templates/nativewind/assets/images/react-logo.png +0 -0
  25. package/templates/nativewind/assets/images/react-logo@2x.png +0 -0
  26. package/templates/nativewind/assets/images/react-logo@3x.png +0 -0
  27. package/templates/nativewind/assets/images/splash-icon.png +0 -0
  28. package/templates/nativewind/assets/images/tabIcons/explore.png +0 -0
  29. package/templates/nativewind/assets/images/tabIcons/explore@2x.png +0 -0
  30. package/templates/nativewind/assets/images/tabIcons/explore@3x.png +0 -0
  31. package/templates/nativewind/assets/images/tabIcons/home.png +0 -0
  32. package/templates/nativewind/assets/images/tabIcons/home@2x.png +0 -0
  33. package/templates/nativewind/assets/images/tabIcons/home@3x.png +0 -0
  34. package/templates/nativewind/assets/images/tutorial-web.png +0 -0
  35. package/templates/nativewind/babel.config.js +9 -0
  36. package/templates/nativewind/expo-env.d.ts +3 -0
  37. package/templates/nativewind/lvcn.json +16 -0
  38. package/templates/nativewind/metro.config.js +6 -0
  39. package/templates/nativewind/nativewind-env.d.ts +3 -0
  40. package/templates/nativewind/package.json +55 -0
  41. package/templates/nativewind/postcss.config.js +6 -0
  42. package/templates/nativewind/scripts/reset-project.js +114 -0
  43. package/templates/nativewind/src/app/_layout.tsx +25 -0
  44. package/templates/nativewind/src/app/explore.tsx +180 -0
  45. package/templates/nativewind/src/app/index.tsx +12 -0
  46. package/templates/nativewind/src/components/animated-icon.module.css +6 -0
  47. package/templates/nativewind/src/components/animated-icon.tsx +148 -0
  48. package/templates/nativewind/src/components/animated-icon.web.tsx +108 -0
  49. package/templates/nativewind/src/components/app-tabs.tsx +32 -0
  50. package/templates/nativewind/src/components/app-tabs.web.tsx +115 -0
  51. package/templates/nativewind/src/components/external-link.tsx +25 -0
  52. package/templates/nativewind/src/components/hint-row.tsx +35 -0
  53. package/templates/nativewind/src/components/themed-text.tsx +73 -0
  54. package/templates/nativewind/src/components/themed-view.tsx +16 -0
  55. package/templates/nativewind/src/components/ui/button.tsx +106 -0
  56. package/templates/nativewind/src/components/ui/collapsible.tsx +65 -0
  57. package/templates/nativewind/src/components/ui/text.tsx +89 -0
  58. package/templates/nativewind/src/components/web-badge.tsx +43 -0
  59. package/templates/nativewind/src/constants/theme.ts +65 -0
  60. package/templates/nativewind/src/global.css +13 -0
  61. package/templates/nativewind/src/hooks/use-color-scheme.ts +1 -0
  62. package/templates/nativewind/src/hooks/use-color-scheme.web.ts +21 -0
  63. package/templates/nativewind/src/hooks/use-theme.ts +14 -0
  64. package/templates/nativewind/tailwind.config.js +86 -0
  65. package/templates/nativewind/tsconfig.json +21 -0
  66. package/templates/uniwind/.claude/settings.json +5 -0
  67. package/templates/uniwind/.vscode/extensions.json +1 -0
  68. package/templates/uniwind/.vscode/settings.json +7 -0
  69. package/templates/uniwind/AGENTS.md +3 -0
  70. package/templates/uniwind/CLAUDE.md +1 -0
  71. package/templates/uniwind/LICENSE +21 -0
  72. package/templates/uniwind/README.md +56 -0
  73. package/templates/uniwind/app.json +42 -0
  74. package/templates/uniwind/assets/expo.icon/Assets/expo-symbol 2.svg +3 -0
  75. package/templates/uniwind/assets/expo.icon/Assets/grid.png +0 -0
  76. package/templates/uniwind/assets/expo.icon/icon.json +40 -0
  77. package/templates/uniwind/assets/images/android-icon-background.png +0 -0
  78. package/templates/uniwind/assets/images/android-icon-foreground.png +0 -0
  79. package/templates/uniwind/assets/images/android-icon-monochrome.png +0 -0
  80. package/templates/uniwind/assets/images/expo-badge-white.png +0 -0
  81. package/templates/uniwind/assets/images/expo-badge.png +0 -0
  82. package/templates/uniwind/assets/images/expo-logo.png +0 -0
  83. package/templates/uniwind/assets/images/favicon.png +0 -0
  84. package/templates/uniwind/assets/images/icon.png +0 -0
  85. package/templates/uniwind/assets/images/logo-glow.png +0 -0
  86. package/templates/uniwind/assets/images/react-logo.png +0 -0
  87. package/templates/uniwind/assets/images/react-logo@2x.png +0 -0
  88. package/templates/uniwind/assets/images/react-logo@3x.png +0 -0
  89. package/templates/uniwind/assets/images/splash-icon.png +0 -0
  90. package/templates/uniwind/assets/images/tabIcons/explore.png +0 -0
  91. package/templates/uniwind/assets/images/tabIcons/explore@2x.png +0 -0
  92. package/templates/uniwind/assets/images/tabIcons/explore@3x.png +0 -0
  93. package/templates/uniwind/assets/images/tabIcons/home.png +0 -0
  94. package/templates/uniwind/assets/images/tabIcons/home@2x.png +0 -0
  95. package/templates/uniwind/assets/images/tabIcons/home@3x.png +0 -0
  96. package/templates/uniwind/assets/images/tutorial-web.png +0 -0
  97. package/templates/uniwind/expo-env.d.ts +3 -0
  98. package/templates/uniwind/lvcn.json +16 -0
  99. package/templates/uniwind/metro.config.js +14 -0
  100. package/templates/uniwind/package.json +46 -0
  101. package/templates/uniwind/scripts/reset-project.js +114 -0
  102. package/templates/uniwind/src/app/_layout.tsx +23 -0
  103. package/templates/uniwind/src/app/explore.tsx +180 -0
  104. package/templates/uniwind/src/app/index.tsx +98 -0
  105. package/templates/uniwind/src/components/animated-icon.module.css +6 -0
  106. package/templates/uniwind/src/components/animated-icon.tsx +148 -0
  107. package/templates/uniwind/src/components/animated-icon.web.tsx +108 -0
  108. package/templates/uniwind/src/components/app-tabs.tsx +32 -0
  109. package/templates/uniwind/src/components/app-tabs.web.tsx +115 -0
  110. package/templates/uniwind/src/components/external-link.tsx +25 -0
  111. package/templates/uniwind/src/components/hint-row.tsx +35 -0
  112. package/templates/uniwind/src/components/themed-text.tsx +73 -0
  113. package/templates/uniwind/src/components/themed-view.tsx +16 -0
  114. package/templates/uniwind/src/components/ui/collapsible.tsx +65 -0
  115. package/templates/uniwind/src/components/web-badge.tsx +43 -0
  116. package/templates/uniwind/src/constants/theme.ts +65 -0
  117. package/templates/uniwind/src/global.css +12 -0
  118. package/templates/uniwind/src/hooks/use-color-scheme.ts +1 -0
  119. package/templates/uniwind/src/hooks/use-color-scheme.web.ts +21 -0
  120. package/templates/uniwind/src/hooks/use-theme.ts +14 -0
  121. package/templates/uniwind/src/uniwind-types.d.ts +10 -0
  122. package/templates/uniwind/tsconfig.json +20 -0
  123. package/.changeset/config.json +0 -17
  124. package/.github/actions/setup/action.yml +0 -21
  125. package/.github/workflows/check.yml +0 -54
  126. package/.github/workflows/release.yml +0 -0
  127. package/.github/workflows/snapshot.yml +0 -24
  128. package/.prettierrc +0 -8
  129. package/.vscode/extensions.json +0 -6
  130. package/.vscode/settings.json +0 -46
  131. package/eslint.config.mjs +0 -118
  132. package/patches/@changesets__get-github-info@0.6.0.patch +0 -48
  133. package/scripts/copy-package-json.ts +0 -35
  134. package/src/bin.ts +0 -18
  135. package/src/cli.ts +0 -66
  136. package/src/contexts/cli-options.ts +0 -29
  137. package/src/project-manifest.ts +0 -370
  138. package/src/services/commands/add.ts +0 -128
  139. package/src/services/commands/doctor.ts +0 -287
  140. package/src/services/commands/init.ts +0 -94
  141. package/src/services/git.ts +0 -39
  142. package/src/services/package-manager.ts +0 -48
  143. package/src/services/project-config.ts +0 -295
  144. package/src/services/required-files-checker.ts +0 -375
  145. package/src/services/spinner.ts +0 -15
  146. package/src/services/template.ts +0 -223
  147. package/src/utils/retry-with.ts +0 -9
  148. package/src/utils/run-command.ts +0 -10
  149. package/test/Dummy.test.ts +0 -7
  150. package/tsconfig.base.json +0 -53
  151. package/tsconfig.json +0 -14
  152. package/tsconfig.scripts.json +0 -17
  153. package/tsconfig.src.json +0 -10
  154. package/tsconfig.test.json +0 -10
  155. package/tsup.config.ts +0 -9
  156. package/vitest.config.ts +0 -12
@@ -0,0 +1,98 @@
1
+ import * as Device from 'expo-device';
2
+ import { Platform, StyleSheet } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
4
+
5
+ import { AnimatedIcon } from '@/components/animated-icon';
6
+ import { HintRow } from '@/components/hint-row';
7
+ import { ThemedText } from '@/components/themed-text';
8
+ import { ThemedView } from '@/components/themed-view';
9
+ import { WebBadge } from '@/components/web-badge';
10
+ import { BottomTabInset, MaxContentWidth, Spacing } from '@/constants/theme';
11
+
12
+ function getDevMenuHint() {
13
+ if (Platform.OS === 'web') {
14
+ return <ThemedText type="small">use browser devtools</ThemedText>;
15
+ }
16
+ if (Device.isDevice) {
17
+ return (
18
+ <ThemedText type="small">
19
+ shake device or press <ThemedText type="code">m</ThemedText> in terminal
20
+ </ThemedText>
21
+ );
22
+ }
23
+ const shortcut = Platform.OS === 'android' ? 'cmd+m (or ctrl+m)' : 'cmd+d';
24
+ return (
25
+ <ThemedText type="small">
26
+ press <ThemedText type="code">{shortcut}</ThemedText>
27
+ </ThemedText>
28
+ );
29
+ }
30
+
31
+ export default function HomeScreen() {
32
+ return (
33
+ <ThemedView style={styles.container}>
34
+ <SafeAreaView style={styles.safeArea}>
35
+ <ThemedView style={styles.heroSection}>
36
+ <AnimatedIcon />
37
+ <ThemedText type="title" style={styles.title}>
38
+ Welcome to&nbsp;Expo
39
+ </ThemedText>
40
+ </ThemedView>
41
+
42
+ <ThemedText type="code" style={styles.code}>
43
+ get started
44
+ </ThemedText>
45
+
46
+ <ThemedView type="backgroundElement" style={styles.stepContainer}>
47
+ <HintRow
48
+ title="Try editing"
49
+ hint={<ThemedText type="code">src/app/index.tsx</ThemedText>}
50
+ />
51
+ <HintRow title="Dev tools" hint={getDevMenuHint()} />
52
+ <HintRow
53
+ title="Fresh start"
54
+ hint={<ThemedText type="code">npm run reset-project</ThemedText>}
55
+ />
56
+ </ThemedView>
57
+
58
+ {Platform.OS === 'web' && <WebBadge />}
59
+ </SafeAreaView>
60
+ </ThemedView>
61
+ );
62
+ }
63
+
64
+ const styles = StyleSheet.create({
65
+ container: {
66
+ flex: 1,
67
+ justifyContent: 'center',
68
+ flexDirection: 'row',
69
+ },
70
+ safeArea: {
71
+ flex: 1,
72
+ paddingHorizontal: Spacing.four,
73
+ alignItems: 'center',
74
+ gap: Spacing.three,
75
+ paddingBottom: BottomTabInset + Spacing.three,
76
+ maxWidth: MaxContentWidth,
77
+ },
78
+ heroSection: {
79
+ alignItems: 'center',
80
+ justifyContent: 'center',
81
+ flex: 1,
82
+ paddingHorizontal: Spacing.four,
83
+ gap: Spacing.four,
84
+ },
85
+ title: {
86
+ textAlign: 'center',
87
+ },
88
+ code: {
89
+ textTransform: 'uppercase',
90
+ },
91
+ stepContainer: {
92
+ gap: Spacing.three,
93
+ alignSelf: 'stretch',
94
+ paddingHorizontal: Spacing.three,
95
+ paddingVertical: Spacing.four,
96
+ borderRadius: Spacing.four,
97
+ },
98
+ });
@@ -0,0 +1,6 @@
1
+ .expoLogoBackground {
2
+ background-image: linear-gradient(180deg, #3c9ffe, #0274df);
3
+ border-radius: 40px;
4
+ width: 128px;
5
+ height: 128px;
6
+ }
@@ -0,0 +1,148 @@
1
+ import { Image } from 'expo-image';
2
+ import * as SplashScreen from 'expo-splash-screen';
3
+ import { useState } from 'react';
4
+ import { Dimensions, StyleSheet, View } from 'react-native';
5
+ import Animated, { Easing, Keyframe } from 'react-native-reanimated';
6
+ import { scheduleOnRN } from 'react-native-worklets';
7
+
8
+ const INITIAL_SCALE_FACTOR = Dimensions.get('screen').height / 90;
9
+ const DURATION = 600;
10
+
11
+ export function AnimatedSplashOverlay() {
12
+ const [animate, setAnimate] = useState(false);
13
+ const [visible, setVisible] = useState(true);
14
+
15
+ if (!visible) return null;
16
+
17
+ const splashKeyframe = new Keyframe({
18
+ 0: {
19
+ transform: [{ scale: 1 }],
20
+ opacity: 1,
21
+ },
22
+ 20: {
23
+ opacity: 1,
24
+ },
25
+ 70: {
26
+ opacity: 0,
27
+ easing: Easing.elastic(0.7),
28
+ },
29
+ 100: {
30
+ opacity: 0,
31
+ transform: [{ scale: 1 }],
32
+ easing: Easing.elastic(0.7),
33
+ },
34
+ });
35
+
36
+ const image = <Image style={styles.image} source={require('@/assets/images/expo-logo.png')} />;
37
+
38
+ return animate ? (
39
+ <Animated.View
40
+ entering={splashKeyframe.duration(DURATION).withCallback((finished) => {
41
+ 'worklet';
42
+ if (finished) {
43
+ scheduleOnRN(setVisible, false);
44
+ }
45
+ })}
46
+ style={styles.splashOverlay}>
47
+ {image}
48
+ </Animated.View>
49
+ ) : (
50
+ <View
51
+ onLayout={() => {
52
+ SplashScreen.hideAsync().finally(() => {
53
+ setAnimate(true);
54
+ });
55
+ }}
56
+ style={styles.splashOverlay}>
57
+ {image}
58
+ </View>
59
+ );
60
+ }
61
+
62
+ const keyframe = new Keyframe({
63
+ 0: {
64
+ transform: [{ scale: INITIAL_SCALE_FACTOR }],
65
+ },
66
+ 100: {
67
+ transform: [{ scale: 1 }],
68
+ easing: Easing.elastic(0.7),
69
+ },
70
+ });
71
+
72
+ const logoKeyframe = new Keyframe({
73
+ 0: {
74
+ transform: [{ scale: 1.3 }],
75
+ opacity: 0,
76
+ },
77
+ 40: {
78
+ transform: [{ scale: 1.3 }],
79
+ opacity: 0,
80
+ easing: Easing.elastic(0.7),
81
+ },
82
+ 100: {
83
+ opacity: 1,
84
+ transform: [{ scale: 1 }],
85
+ easing: Easing.elastic(0.7),
86
+ },
87
+ });
88
+
89
+ const glowKeyframe = new Keyframe({
90
+ 0: {
91
+ transform: [{ rotateZ: '0deg' }],
92
+ },
93
+ 100: {
94
+ transform: [{ rotateZ: '7200deg' }],
95
+ },
96
+ });
97
+
98
+ export function AnimatedIcon() {
99
+ return (
100
+ <View style={styles.iconContainer}>
101
+ <Animated.View entering={glowKeyframe.duration(60 * 1000 * 4)} style={styles.glow}>
102
+ <Image style={styles.glow} source={require('@/assets/images/logo-glow.png')} />
103
+ </Animated.View>
104
+
105
+ <Animated.View entering={keyframe.duration(DURATION)} style={styles.background} />
106
+ <Animated.View style={styles.imageContainer} entering={logoKeyframe.duration(DURATION)}>
107
+ <Image style={styles.image} source={require('@/assets/images/expo-logo.png')} />
108
+ </Animated.View>
109
+ </View>
110
+ );
111
+ }
112
+
113
+ const styles = StyleSheet.create({
114
+ imageContainer: {
115
+ justifyContent: 'center',
116
+ alignItems: 'center',
117
+ },
118
+ glow: {
119
+ width: 201,
120
+ height: 201,
121
+ position: 'absolute',
122
+ },
123
+ iconContainer: {
124
+ justifyContent: 'center',
125
+ alignItems: 'center',
126
+ width: 128,
127
+ height: 128,
128
+ zIndex: 100,
129
+ },
130
+ image: {
131
+ width: 76,
132
+ height: 71,
133
+ },
134
+ background: {
135
+ borderRadius: 40,
136
+ experimental_backgroundImage: `linear-gradient(180deg, #3C9FFE, #0274DF)`,
137
+ width: 128,
138
+ height: 128,
139
+ position: 'absolute',
140
+ },
141
+ splashOverlay: {
142
+ ...StyleSheet.absoluteFill,
143
+ backgroundColor: '#208AEF',
144
+ alignItems: 'center',
145
+ justifyContent: 'center',
146
+ zIndex: 1000,
147
+ },
148
+ });
@@ -0,0 +1,108 @@
1
+ import { Image } from 'expo-image';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import Animated, { Keyframe, Easing } from 'react-native-reanimated';
4
+
5
+ import classes from './animated-icon.module.css';
6
+ const DURATION = 300;
7
+
8
+ export function AnimatedSplashOverlay() {
9
+ return null;
10
+ }
11
+
12
+ const keyframe = new Keyframe({
13
+ 0: {
14
+ transform: [{ scale: 0 }],
15
+ },
16
+ 60: {
17
+ transform: [{ scale: 1.2 }],
18
+ easing: Easing.elastic(1.2),
19
+ },
20
+ 100: {
21
+ transform: [{ scale: 1 }],
22
+ easing: Easing.elastic(1.2),
23
+ },
24
+ });
25
+
26
+ const logoKeyframe = new Keyframe({
27
+ 0: {
28
+ opacity: 0,
29
+ },
30
+ 60: {
31
+ transform: [{ scale: 1.2 }],
32
+ opacity: 0,
33
+ easing: Easing.elastic(1.2),
34
+ },
35
+ 100: {
36
+ transform: [{ scale: 1 }],
37
+ opacity: 1,
38
+ easing: Easing.elastic(1.2),
39
+ },
40
+ });
41
+
42
+ const glowKeyframe = new Keyframe({
43
+ 0: {
44
+ transform: [{ rotateZ: '-180deg' }, { scale: 0.8 }],
45
+ opacity: 0,
46
+ },
47
+ [DURATION / 1000]: {
48
+ transform: [{ rotateZ: '0deg' }, { scale: 1 }],
49
+ opacity: 1,
50
+ easing: Easing.elastic(0.7),
51
+ },
52
+ 100: {
53
+ transform: [{ rotateZ: '7200deg' }],
54
+ },
55
+ });
56
+
57
+ export function AnimatedIcon() {
58
+ return (
59
+ <View style={styles.iconContainer}>
60
+ <Animated.View entering={glowKeyframe.duration(60 * 1000 * 4)} style={styles.glow}>
61
+ <Image style={styles.glow} source={require('@/assets/images/logo-glow.png')} />
62
+ </Animated.View>
63
+
64
+ <Animated.View style={styles.background} entering={keyframe.duration(DURATION)}>
65
+ <div className={classes.expoLogoBackground} />
66
+ </Animated.View>
67
+
68
+ <Animated.View style={styles.imageContainer} entering={logoKeyframe.duration(DURATION)}>
69
+ <Image style={styles.image} source={require('@/assets/images/expo-logo.png')} />
70
+ </Animated.View>
71
+ </View>
72
+ );
73
+ }
74
+
75
+ const styles = StyleSheet.create({
76
+ container: {
77
+ alignItems: 'center',
78
+ width: '100%',
79
+ zIndex: 1000,
80
+ position: 'absolute',
81
+ top: 128 / 2 + 138,
82
+ },
83
+ imageContainer: {
84
+ justifyContent: 'center',
85
+ alignItems: 'center',
86
+ },
87
+ glow: {
88
+ width: 201,
89
+ height: 201,
90
+ position: 'absolute',
91
+ },
92
+ iconContainer: {
93
+ justifyContent: 'center',
94
+ alignItems: 'center',
95
+ width: 128,
96
+ height: 128,
97
+ },
98
+ image: {
99
+ position: 'absolute',
100
+ width: 76,
101
+ height: 71,
102
+ },
103
+ background: {
104
+ width: 128,
105
+ height: 128,
106
+ position: 'absolute',
107
+ },
108
+ });
@@ -0,0 +1,32 @@
1
+ import { NativeTabs } from 'expo-router/unstable-native-tabs';
2
+ import { useColorScheme } from 'react-native';
3
+
4
+ import { Colors } from '@/constants/theme';
5
+
6
+ export default function AppTabs() {
7
+ const scheme = useColorScheme();
8
+ const colors = Colors[scheme === 'unspecified' ? 'light' : scheme];
9
+
10
+ return (
11
+ <NativeTabs
12
+ backgroundColor={colors.background}
13
+ indicatorColor={colors.backgroundElement}
14
+ labelStyle={{ selected: { color: colors.text } }}>
15
+ <NativeTabs.Trigger name="index">
16
+ <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
17
+ <NativeTabs.Trigger.Icon
18
+ src={require('@/assets/images/tabIcons/home.png')}
19
+ renderingMode="template"
20
+ />
21
+ </NativeTabs.Trigger>
22
+
23
+ <NativeTabs.Trigger name="explore">
24
+ <NativeTabs.Trigger.Label>Explore</NativeTabs.Trigger.Label>
25
+ <NativeTabs.Trigger.Icon
26
+ src={require('@/assets/images/tabIcons/explore.png')}
27
+ renderingMode="template"
28
+ />
29
+ </NativeTabs.Trigger>
30
+ </NativeTabs>
31
+ );
32
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ Tabs,
3
+ TabList,
4
+ TabTrigger,
5
+ TabSlot,
6
+ TabTriggerSlotProps,
7
+ TabListProps,
8
+ } from 'expo-router/ui';
9
+ import { SymbolView } from 'expo-symbols';
10
+ import { Pressable, useColorScheme, View, StyleSheet } from 'react-native';
11
+
12
+ import { ExternalLink } from './external-link';
13
+ import { ThemedText } from './themed-text';
14
+ import { ThemedView } from './themed-view';
15
+
16
+ import { Colors, MaxContentWidth, Spacing } from '@/constants/theme';
17
+
18
+ export default function AppTabs() {
19
+ return (
20
+ <Tabs>
21
+ <TabSlot style={{ height: '100%' }} />
22
+ <TabList asChild>
23
+ <CustomTabList>
24
+ <TabTrigger name="home" href="/" asChild>
25
+ <TabButton>Home</TabButton>
26
+ </TabTrigger>
27
+ <TabTrigger name="explore" href="/explore" asChild>
28
+ <TabButton>Explore</TabButton>
29
+ </TabTrigger>
30
+ </CustomTabList>
31
+ </TabList>
32
+ </Tabs>
33
+ );
34
+ }
35
+
36
+ export function TabButton({ children, isFocused, ...props }: TabTriggerSlotProps) {
37
+ return (
38
+ <Pressable {...props} style={({ pressed }) => pressed && styles.pressed}>
39
+ <ThemedView
40
+ type={isFocused ? 'backgroundSelected' : 'backgroundElement'}
41
+ style={styles.tabButtonView}>
42
+ <ThemedText type="small" themeColor={isFocused ? 'text' : 'textSecondary'}>
43
+ {children}
44
+ </ThemedText>
45
+ </ThemedView>
46
+ </Pressable>
47
+ );
48
+ }
49
+
50
+ export function CustomTabList(props: TabListProps) {
51
+ const scheme = useColorScheme();
52
+ const colors = Colors[scheme === 'unspecified' ? 'light' : scheme];
53
+
54
+ return (
55
+ <View {...props} style={styles.tabListContainer}>
56
+ <ThemedView type="backgroundElement" style={styles.innerContainer}>
57
+ <ThemedText type="smallBold" style={styles.brandText}>
58
+ Expo Starter
59
+ </ThemedText>
60
+
61
+ {props.children}
62
+
63
+ <ExternalLink href="https://docs.expo.dev" asChild>
64
+ <Pressable style={styles.externalPressable}>
65
+ <ThemedText type="link">Docs</ThemedText>
66
+ <SymbolView
67
+ tintColor={colors.text}
68
+ name={{ ios: 'arrow.up.right.square', web: 'link' }}
69
+ size={12}
70
+ />
71
+ </Pressable>
72
+ </ExternalLink>
73
+ </ThemedView>
74
+ </View>
75
+ );
76
+ }
77
+
78
+ const styles = StyleSheet.create({
79
+ tabListContainer: {
80
+ position: 'absolute',
81
+ width: '100%',
82
+ padding: Spacing.three,
83
+ justifyContent: 'center',
84
+ alignItems: 'center',
85
+ flexDirection: 'row',
86
+ },
87
+ innerContainer: {
88
+ paddingVertical: Spacing.two,
89
+ paddingHorizontal: Spacing.five,
90
+ borderRadius: Spacing.five,
91
+ flexDirection: 'row',
92
+ alignItems: 'center',
93
+ flexGrow: 1,
94
+ gap: Spacing.two,
95
+ maxWidth: MaxContentWidth,
96
+ },
97
+ brandText: {
98
+ marginRight: 'auto',
99
+ },
100
+ pressed: {
101
+ opacity: 0.7,
102
+ },
103
+ tabButtonView: {
104
+ paddingVertical: Spacing.one,
105
+ paddingHorizontal: Spacing.three,
106
+ borderRadius: Spacing.three,
107
+ },
108
+ externalPressable: {
109
+ flexDirection: 'row',
110
+ justifyContent: 'center',
111
+ alignItems: 'center',
112
+ gap: Spacing.one,
113
+ marginLeft: Spacing.three,
114
+ },
115
+ });
@@ -0,0 +1,25 @@
1
+ import { Href, Link } from 'expo-router';
2
+ import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
3
+ import { type ComponentProps } from 'react';
4
+
5
+ type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
6
+
7
+ export function ExternalLink({ href, ...rest }: Props) {
8
+ return (
9
+ <Link
10
+ target="_blank"
11
+ {...rest}
12
+ href={href}
13
+ onPress={async (event) => {
14
+ if (process.env.EXPO_OS !== 'web') {
15
+ // Prevent the default behavior of linking to the default browser on native.
16
+ event.preventDefault();
17
+ // Open the link in an in-app browser.
18
+ await openBrowserAsync(href, {
19
+ presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
20
+ });
21
+ }
22
+ }}
23
+ />
24
+ );
25
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+
4
+ import { ThemedText } from './themed-text';
5
+ import { ThemedView } from './themed-view';
6
+
7
+ import { Spacing } from '@/constants/theme';
8
+
9
+ type HintRowProps = {
10
+ title?: string;
11
+ hint?: ReactNode;
12
+ };
13
+
14
+ export function HintRow({ title = 'Try editing', hint = 'app/index.tsx' }: HintRowProps) {
15
+ return (
16
+ <View style={styles.stepRow}>
17
+ <ThemedText type="small">{title}</ThemedText>
18
+ <ThemedView type="backgroundSelected" style={styles.codeSnippet}>
19
+ <ThemedText themeColor="textSecondary">{hint}</ThemedText>
20
+ </ThemedView>
21
+ </View>
22
+ );
23
+ }
24
+
25
+ const styles = StyleSheet.create({
26
+ stepRow: {
27
+ flexDirection: 'row',
28
+ justifyContent: 'space-between',
29
+ },
30
+ codeSnippet: {
31
+ borderRadius: Spacing.two,
32
+ paddingVertical: Spacing.half,
33
+ paddingHorizontal: Spacing.two,
34
+ },
35
+ });
@@ -0,0 +1,73 @@
1
+ import { Platform, StyleSheet, Text, type TextProps } from 'react-native';
2
+
3
+ import { Fonts, ThemeColor } from '@/constants/theme';
4
+ import { useTheme } from '@/hooks/use-theme';
5
+
6
+ export type ThemedTextProps = TextProps & {
7
+ type?: 'default' | 'title' | 'small' | 'smallBold' | 'subtitle' | 'link' | 'linkPrimary' | 'code';
8
+ themeColor?: ThemeColor;
9
+ };
10
+
11
+ export function ThemedText({ style, type = 'default', themeColor, ...rest }: ThemedTextProps) {
12
+ const theme = useTheme();
13
+
14
+ return (
15
+ <Text
16
+ style={[
17
+ { color: theme[themeColor ?? 'text'] },
18
+ type === 'default' && styles.default,
19
+ type === 'title' && styles.title,
20
+ type === 'small' && styles.small,
21
+ type === 'smallBold' && styles.smallBold,
22
+ type === 'subtitle' && styles.subtitle,
23
+ type === 'link' && styles.link,
24
+ type === 'linkPrimary' && styles.linkPrimary,
25
+ type === 'code' && styles.code,
26
+ style,
27
+ ]}
28
+ {...rest}
29
+ />
30
+ );
31
+ }
32
+
33
+ const styles = StyleSheet.create({
34
+ small: {
35
+ fontSize: 14,
36
+ lineHeight: 20,
37
+ fontWeight: 500,
38
+ },
39
+ smallBold: {
40
+ fontSize: 14,
41
+ lineHeight: 20,
42
+ fontWeight: 700,
43
+ },
44
+ default: {
45
+ fontSize: 16,
46
+ lineHeight: 24,
47
+ fontWeight: 500,
48
+ },
49
+ title: {
50
+ fontSize: 48,
51
+ fontWeight: 600,
52
+ lineHeight: 52,
53
+ },
54
+ subtitle: {
55
+ fontSize: 32,
56
+ lineHeight: 44,
57
+ fontWeight: 600,
58
+ },
59
+ link: {
60
+ lineHeight: 30,
61
+ fontSize: 14,
62
+ },
63
+ linkPrimary: {
64
+ lineHeight: 30,
65
+ fontSize: 14,
66
+ color: '#3c87f7',
67
+ },
68
+ code: {
69
+ fontFamily: Fonts.mono,
70
+ fontWeight: Platform.select({ android: 700 }) ?? 500,
71
+ fontSize: 12,
72
+ },
73
+ });
@@ -0,0 +1,16 @@
1
+ import { View, type ViewProps } from 'react-native';
2
+
3
+ import { ThemeColor } from '@/constants/theme';
4
+ import { useTheme } from '@/hooks/use-theme';
5
+
6
+ export type ThemedViewProps = ViewProps & {
7
+ lightColor?: string;
8
+ darkColor?: string;
9
+ type?: ThemeColor;
10
+ };
11
+
12
+ export function ThemedView({ style, lightColor, darkColor, type, ...otherProps }: ThemedViewProps) {
13
+ const theme = useTheme();
14
+
15
+ return <View style={[{ backgroundColor: theme[type ?? 'background'] }, style]} {...otherProps} />;
16
+ }