init: 初始化模板项目

This commit is contained in:
2025-08-25 11:57:14 +08:00
commit aaa949bd7d
90 changed files with 20722 additions and 0 deletions

3
packages/app/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
.DS_Store
THUMBS_DB

View File

@@ -0,0 +1,127 @@
import {
Anchor,
Button,
H1,
Paragraph,
Separator,
Sheet,
SwitchRouterButton,
SwitchThemeButton,
useToastController,
XStack,
YStack
} from '@my/ui'
import { ChevronDown, ChevronUp } from '@tamagui/lucide-icons'
import { useState } from 'react'
import { Platform } from 'react-native'
import { useLink } from 'solito/navigation'
export function HomeScreen({ pagesMode = false }: { pagesMode?: boolean }) {
const linkTarget = pagesMode ? '/pages-example-user' : '/user'
const linkProps = useLink({
href: `${linkTarget}/nate`,
})
return (
<YStack flex={1} justify="center" items="center" gap="$8" p="$4" bg="$background">
<XStack
position="absolute"
width="100%"
t="$6"
gap="$6"
justify="center"
flexWrap="wrap"
$sm={{ position: 'relative', t: 0 }}
>
{Platform.OS === 'web' && (
<>
<SwitchRouterButton pagesMode={pagesMode} />
<SwitchThemeButton />
</>
)}
</XStack>
<YStack gap="$4">
<H1 text="center" color="$color12">
Welcome to Tamagui.
</H1>
<Paragraph color="$color10" text="center">
Here's a basic starter to show navigating from one screen to another.
</Paragraph>
<Separator />
<Paragraph text="center">
This screen uses the same code on Next.js and React Native.
</Paragraph>
<Separator />
</YStack>
<Button {...linkProps}>Link to user</Button>
<SheetDemo />
</YStack>
)
}
function SheetDemo() {
const toast = useToastController()
const [open, setOpen] = useState(false)
const [position, setPosition] = useState(0)
return (
<>
<Button
size="$6"
icon={open ? ChevronDown : ChevronUp}
circular
onPress={() => setOpen((x) => !x)}
/>
<Sheet
modal
animation="medium"
open={open}
onOpenChange={setOpen}
snapPoints={[80]}
position={position}
onPositionChange={setPosition}
dismissOnSnapToBottom
>
<Sheet.Overlay
bg="$shadow4"
animation="lazy"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
/>
<Sheet.Handle bg="$color8" />
<Sheet.Frame items="center" justify="center" gap="$10" bg="$color2">
<XStack gap="$2">
<Paragraph text="center">Made by</Paragraph>
<Anchor color="$blue10" href="https://twitter.com/natebirdman" target="_blank">
@natebirdman,
</Anchor>
<Anchor
color="$blue10"
href="https://github.com/tamagui/tamagui"
target="_blank"
rel="noreferrer"
>
give it a ⭐️
</Anchor>
</XStack>
<Button
size="$6"
circular
icon={ChevronDown}
onPress={() => {
setOpen(false)
toast.show('Sheet closed!', {
message: 'Just showing how toast works...',
})
}}
/>
</Sheet.Frame>
</Sheet>
</>
)
}

View File

@@ -0,0 +1,18 @@
import { Button, Paragraph, YStack } from '@my/ui'
import { ChevronLeft } from '@tamagui/lucide-icons'
import { useRouter } from 'solito/navigation'
export function UserDetailScreen({ id }: { id: string }) {
const router = useRouter()
if (!id) {
return null
}
return (
<YStack flex={1} justify="center" items="center" gap="$4" bg="$background">
<Paragraph text="center" fontWeight="700" color="$blue10">{`User ID: ${id}`}</Paragraph>
<Button icon={ChevronLeft} onPress={() => router.back()}>
Go Home
</Button>
</YStack>
)
}

4
packages/app/index.ts Normal file
View File

@@ -0,0 +1,4 @@
// leave this blank
// don't re-export files from this workspace. it'll break next.js tree shaking
// https://github.com/vercel/next.js/issues/12557
export {}

28
packages/app/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"version": "0.0.0",
"name": "app",
"main": "index.ts",
"private": true,
"sideEffects": [
"*.css"
],
"dependencies": {
"@my/ui": "0.0.1",
"@react-navigation/native": "^6.1.17",
"@tamagui/animations-react-native": "^1.132.18",
"@tamagui/colors": "^1.132.18",
"@tamagui/font-inter": "^1.132.18",
"@tamagui/lucide-icons": "^1.132.18",
"@tamagui/shorthands": "^1.132.18",
"@tamagui/themes": "^1.132.18",
"burnt": "^0.12.2",
"expo-constants": "~17.1.6",
"expo-linking": "~7.1.4",
"react-native-safe-area-context": "5.4.0",
"solito": "^4.2.2"
},
"devDependencies": {
"@types/react": "~19.0.10",
"@types/react-native": "^0.73.0"
}
}

View File

@@ -0,0 +1,64 @@
'use client'
import '@tamagui/core/reset.css'
import '@tamagui/font-inter/css/400.css'
import '@tamagui/font-inter/css/700.css'
import '@tamagui/polyfill-dev'
import type { ReactNode } from 'react'
import { useServerInsertedHTML } from 'next/navigation'
import { NextThemeProvider, useRootTheme } from '@tamagui/next-theme'
import { config } from '@my/ui'
import { Provider } from 'app/provider'
import { StyleSheet } from 'react-native'
export const NextTamaguiProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useRootTheme()
useServerInsertedHTML(() => {
// @ts-ignore
const rnwStyle = StyleSheet.getSheet()
return (
<>
<link rel="stylesheet" href="/tamagui.css" />
<style dangerouslySetInnerHTML={{ __html: rnwStyle.textContent }} id={rnwStyle.id} />
<style
dangerouslySetInnerHTML={{
// the first time this runs you'll get the full CSS including all themes
// after that, it will only return CSS generated since the last call
__html: config.getNewCSS(),
}}
/>
<style
dangerouslySetInnerHTML={{
__html: config.getCSS({
exclude: process.env.NODE_ENV === 'production' ? 'design-system' : null,
}),
}}
/>
<script
dangerouslySetInnerHTML={{
// avoid flash of animated things on enter:
__html: `document.documentElement.classList.add('t_unmounted')`,
}}
/>
</>
)
})
return (
<NextThemeProvider
skipNextHead
defaultTheme="light"
onChangeTheme={(next) => {
setTheme(next as any)
}}
>
<Provider disableRootThemeClass defaultTheme={theme || 'light'}>
{children}
</Provider>
</NextThemeProvider>
)
}

View File

@@ -0,0 +1,11 @@
import { ToastViewport as ToastViewportOg } from '@my/ui'
export const ToastViewport = () => {
return (
<ToastViewportOg
top="$8"
left={0}
right={0}
/>
)
}

View File

@@ -0,0 +1,11 @@
import { ToastViewport as ToastViewportOg } from '@my/ui'
export const ToastViewport = () => {
return (
<ToastViewportOg
left={0}
right={0}
top={10}
/>
)
}

View File

@@ -0,0 +1,29 @@
import { useColorScheme } from 'react-native'
import {
CustomToast,
TamaguiProvider,
type TamaguiProviderProps,
ToastProvider,
config,
isWeb,
} from '@my/ui'
import { ToastViewport } from './ToastViewport'
export function Provider({
children,
defaultTheme = 'light',
...rest
}: Omit<TamaguiProviderProps, 'config'> & { defaultTheme?: string }) {
const colorScheme = useColorScheme()
const theme = defaultTheme || (colorScheme === 'dark' ? 'dark' : 'light')
return (
<TamaguiProvider config={config} defaultTheme={theme} {...rest}>
<ToastProvider swipeDirection="horizontal" duration={6000} native={isWeb ? [] : ['mobile']}>
{children}
<CustomToast />
<ToastViewport />
</ToastProvider>
</TamaguiProvider>
)
}

View File

@@ -0,0 +1 @@
export { SafeAreaProvider as SafeArea } from 'react-native-safe-area-context'

View File

@@ -0,0 +1,10 @@
// on Web, we don't use React Navigation, so we are going to avoid the safe area provider
// instead, we just have a no-op here
// for more, see: https://solito.dev/recipes/tree-shaking
// if you need safe area hooks yourself, you can implement this yourself
// however, you may be better off using the CSS selector for env(safe-area-inset-top) on Web
// for more, see the `./use-safe-area.web.ts` file
export const SafeArea = ({ children }: { children: React.ReactElement }) => <>{children}</>

View File

@@ -0,0 +1,6 @@
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const useSafeArea = useSafeAreaInsets
// `export { useSafeAreaInsets as useSafeArea }` breaks autoimport, so do this instead
export { useSafeArea }

View File

@@ -0,0 +1,31 @@
// I don't use the real useSafeAreaInsets() hook, since
// 1) the SafeAreaProvider forces you to render null on Web until it measures
// 2) you might not need to support it, unless you're doing landscape stuff
// 3) react-native-safe-area-context has a massive import on Web
// see: https://github.com/th3rdwave/react-native-safe-area-context/pull/189#issuecomment-815274313
// 4) most importantly, I think you can just use the env(safe-area-inset-bottom) CSS variable instead
// after all, safe area code is few-and-far-between, so if you have to write some platform-speciifc code for it,
// that is probably better than a massive bundle size for little benefit
import type { useSafeArea as nativeHook } from './use-safe-area'
const area = {
bottom: 0,
left: 0,
right: 0,
top: 0,
// you could also use CSS env variables like below:
// but you'll have to be sure to override the types for `useSafeArea`
// and make sure to never add numbers and strings when you consue useSafeArea
// just keep in mind that the env() doesn't work on older browsers I think
// top: `env(safe-area-inset-top)`,
// right: `env(safe-area-inset-right)`,
// bottom: `env(safe-area-inset-bottom)`,
// left: `env(safe-area-inset-left)`,
}
export function useSafeArea(): ReturnType<typeof nativeHook> {
return area
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base",
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"paths": {
"app/*": ["./packages/app/*"],
"@my/ui/*": ["./packages/ui/*"]
}
},
"references": []
}

7
packages/app/types.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
import { config } from '@my/config'
export type Conf = typeof config
declare module '@my/ui' {
interface TamaguiCustomConfig extends Conf {}
}