chore: scaffold expo project

This commit is contained in:
Rodrigo Verdiani 2026-04-15 12:51:07 -03:00
commit be245e6d0e
28 changed files with 11818 additions and 0 deletions

575
.gitignore vendored Normal file
View File

@ -0,0 +1,575 @@
# Created by https://www.toptal.com/developers/gitignore/api/react,reactnative,jetbrains,jetbrains+iml,jetbrains+all,visualstudiocode,linux,macos,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=react,reactnative,jetbrains,jetbrains+iml,jetbrains+all,visualstudiocode,linux,macos,windows
### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### JetBrains+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# AWS User-specific
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# SonarLint plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### react ###
.DS_*
*.log
logs
**/*.backup.*
**/*.back.*
node_modules
bower_components
*.sublime*
psd
thumb
sketch
### ReactNative ###
# React Native Stack Base
.expo
__generated__
### ReactNative.macOS Stack ###
# General
# Icon must end with two \r
# Thumbnails
# Files that might appear in the root of a volume
# Directories potentially created on remote AFP share
### ReactNative.Android Stack ###
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
### ReactNative.Gradle Stack ###
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### ReactNative.Xcode Stack ###
## User settings
xcuserdata/
## Xcode 8 and earlier
*.xcscmblueprint
*.xccheckout
### ReactNative.Linux Stack ###
# temporary files which can be created if a process still has a handle open of a deleted file
# KDE directory preferences
# Linux trash folder which might appear on any partition or disk
# .nfs files are created when an open file is removed but is still being accessed
### ReactNative.Node Stack ###
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### ReactNative.Buck Stack ###
buck-out/
.buckconfig.local
.buckd/
.buckversion
.fakebuckversion
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/react,reactnative,jetbrains,jetbrains+iml,jetbrains+all,visualstudiocode,linux,macos,windows
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli
android/*

9
.prettierrc Normal file
View File

@ -0,0 +1,9 @@
{
"printWidth": 100,
"tabWidth": 2,
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["cva"]
}

73
README.md Normal file
View File

@ -0,0 +1,73 @@
# Minimal Template
This is a [React Native](https://reactnative.dev/) project built with [Expo](https://expo.dev/) and [React Native Reusables](https://reactnativereusables.com).
It was initialized using the following command, then the `Minimal (Nativewind)` template was selected when prompted:
```bash
npx @react-native-reusables/cli@latest init
```
## Getting Started
To run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
This will start the Expo Dev Server. Open the app in:
- **iOS**: press `i` to launch in the iOS simulator _(Mac only)_
- **Android**: press `a` to launch in the Android emulator
- **Web**: press `w` to run in a browser
You can also scan the QR code using the [Expo Go](https://expo.dev/go) app on your device. This project fully supports running in Expo Go for quick testing on physical devices.
## Adding components
You can add more reusable components using the CLI:
```bash
npx react-native-reusables/cli@latest add [...components]
```
> e.g. `npx react-native-reusables/cli@latest add input textarea`
If you don't specify any component names, you'll be prompted to select which components to add interactively. Use the `--all` flag to install all available components at once.
## Project Features
- ⚛️ Built with [Expo Router](https://expo.dev/router)
- 🎨 Styled with [Tailwind CSS](https://tailwindcss.com/) via [Nativewind](https://www.nativewind.dev/)
- 📦 UI powered by [React Native Reusables](https://github.com/founded-labs/react-native-reusables)
- 🚀 New Architecture enabled
- 🔥 Edge to Edge enabled
- 📱 Runs on iOS, Android, and Web
## Learn More
To dive deeper into the technologies used:
- [React Native Docs](https://reactnative.dev/docs/getting-started)
- [Expo Docs](https://docs.expo.dev/)
- [Nativewind Docs](https://www.nativewind.dev/)
- [React Native Reusables](https://reactnativereusables.com)
## Deploy with EAS
The easiest way to deploy your app is with [Expo Application Services (EAS)](https://expo.dev/eas).
- [EAS Build](https://docs.expo.dev/build/introduction/)
- [EAS Updates](https://docs.expo.dev/eas-update/introduction/)
- [EAS Submit](https://docs.expo.dev/submit/introduction/)
---
If you enjoy using React Native Reusables, please consider giving it a ⭐ on [GitHub](https://github.com/founded-labs/react-native-reusables). Your support means a lot!

39
app.json Normal file
View File

@ -0,0 +1,39 @@
{
"expo": {
"name": "mangamochi",
"slug": "mangamochi",
"version": "1.0.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "mangamochi",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router"
],
"experiments": {
"typedRoutes": true
}
}
}

27
app/+html.tsx Normal file
View File

@ -0,0 +1,27 @@
import { ScrollViewStyleReset } from 'expo-router/html';
import { type PropsWithChildren } from 'react';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: PropsWithChildren) {
return (
<html lang="en" className="bg-background">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}

18
app/+not-found.tsx Normal file
View File

@ -0,0 +1,18 @@
import { Link, Stack } from 'expo-router';
import { View } from 'react-native';
import { Text } from '@/components/ui/text';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View>
<Text>This screen doesn't exist.</Text>
<Link href="/">
<Text>Go to home screen!</Text>
</Link>
</View>
</>
);
}

25
app/_layout.tsx Normal file
View File

@ -0,0 +1,25 @@
import '@/global.css';
import { NAV_THEME } from '@/lib/theme';
import { ThemeProvider } from '@react-navigation/native';
import { PortalHost } from '@rn-primitives/portal';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useColorScheme } from 'nativewind';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export default function RootLayout() {
const { colorScheme } = useColorScheme();
return (
<ThemeProvider value={NAV_THEME[colorScheme ?? 'light']}>
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
<Stack />
<PortalHost />
</ThemeProvider>
);
}

77
app/index.tsx Normal file
View File

@ -0,0 +1,77 @@
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text';
import { Link, Stack } from 'expo-router';
import { MoonStarIcon, StarIcon, SunIcon } from 'lucide-react-native';
import { useColorScheme } from 'nativewind';
import * as React from 'react';
import { Image, type ImageStyle, View } from 'react-native';
const LOGO = {
light: require('@/assets/images/react-native-reusables-light.png'),
dark: require('@/assets/images/react-native-reusables-dark.png'),
};
const SCREEN_OPTIONS = {
title: 'React Native Reusables',
headerTransparent: true,
headerRight: () => <ThemeToggle />,
};
const IMAGE_STYLE: ImageStyle = {
height: 76,
width: 76,
};
export default function Screen() {
const { colorScheme } = useColorScheme();
return (
<>
<Stack.Screen options={SCREEN_OPTIONS} />
<View className="flex-1 items-center justify-center gap-8 p-4">
<Image source={LOGO[colorScheme ?? 'light']} style={IMAGE_STYLE} resizeMode="contain" />
<View className="gap-2 p-4">
<Text className="ios:text-foreground font-mono text-sm text-muted-foreground">
1. Edit <Text variant="code">app/index.tsx</Text> to get started.
</Text>
<Text className="ios:text-foreground font-mono text-sm text-muted-foreground">
2. Save to see your changes instantly.
</Text>
</View>
<View className="flex-row gap-2">
<Link href="https://reactnativereusables.com" asChild>
<Button>
<Text>Browse the Docs</Text>
</Button>
</Link>
<Link href="https://github.com/founded-labs/react-native-reusables" asChild>
<Button variant="ghost">
<Text>Star the Repo</Text>
<Icon as={StarIcon} />
</Button>
</Link>
</View>
</View>
</>
);
}
const THEME_ICONS = {
light: SunIcon,
dark: MoonStarIcon,
};
function ThemeToggle() {
const { colorScheme, toggleColorScheme } = useColorScheme();
return (
<Button
onPressIn={toggleColorScheme}
size="icon"
variant="ghost"
className="ios:size-9 rounded-full web:mx-4">
<Icon as={THEME_ICONS[colorScheme ?? 'light']} className="size-5" />
</Button>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
};
};

19
components.json Normal file
View File

@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "global.css",
"baseColor": "neutral",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

108
components/ui/button.tsx Normal file
View File

@ -0,0 +1,108 @@
import { TextClassContext } from '@/components/ui/text';
import { cn } from '@/lib/utils';
import { cva, type VariantProps } from 'class-variance-authority';
import { Platform, Pressable } from 'react-native';
const buttonVariants = cva(
cn(
'group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none',
Platform.select({
web: "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
})
),
{
variants: {
variant: {
default: cn(
'bg-primary shadow-sm shadow-black/5 active:bg-primary/90',
Platform.select({ web: 'hover:bg-primary/90' })
),
destructive: cn(
'bg-destructive shadow-sm shadow-black/5 active:bg-destructive/90 dark:bg-destructive/60',
Platform.select({
web: 'hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
})
),
outline: cn(
'border border-border bg-background shadow-sm shadow-black/5 active:bg-accent dark:border-input dark:bg-input/30 dark:active:bg-input/50',
Platform.select({
web: 'hover:bg-accent dark:hover:bg-input/50',
})
),
secondary: cn(
'bg-secondary shadow-sm shadow-black/5 active:bg-secondary/80',
Platform.select({ web: 'hover:bg-secondary/80' })
),
ghost: cn(
'active:bg-accent dark:active:bg-accent/50',
Platform.select({ web: 'hover:bg-accent dark:hover:bg-accent/50' })
),
link: '',
},
size: {
default: cn('h-10 px-4 py-2 sm:h-9', Platform.select({ web: 'has-[>svg]:px-3' })),
sm: cn('h-9 gap-1.5 rounded-md px-3 sm:h-8', Platform.select({ web: 'has-[>svg]:px-2.5' })),
lg: cn('h-11 rounded-md px-6 sm:h-10', Platform.select({ web: 'has-[>svg]:px-4' })),
icon: 'h-10 w-10 sm:h-9 sm:w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
const buttonTextVariants = cva(
cn(
'text-sm font-medium text-foreground',
Platform.select({ web: 'pointer-events-none transition-colors' })
),
{
variants: {
variant: {
default: 'text-primary-foreground',
destructive: 'text-white',
outline: cn(
'group-active:text-accent-foreground',
Platform.select({ web: 'group-hover:text-accent-foreground' })
),
secondary: 'text-secondary-foreground',
ghost: 'group-active:text-accent-foreground',
link: cn(
'text-primary group-active:underline',
Platform.select({ web: 'underline-offset-4 hover:underline group-hover:underline' })
),
},
size: {
default: '',
sm: '',
lg: '',
icon: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
type ButtonProps = React.ComponentProps<typeof Pressable> &
React.RefAttributes<typeof Pressable> &
VariantProps<typeof buttonVariants>;
function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
<Pressable
className={cn(props.disabled && 'opacity-50', buttonVariants({ variant, size }), className)}
role="button"
{...props}
/>
</TextClassContext.Provider>
);
}
export { Button, buttonTextVariants, buttonVariants };
export type { ButtonProps };

54
components/ui/icon.tsx Normal file
View File

@ -0,0 +1,54 @@
import { cn } from '@/lib/utils';
import type { LucideIcon, LucideProps } from 'lucide-react-native';
import { cssInterop } from 'nativewind';
type IconProps = LucideProps & {
as: LucideIcon;
};
function IconImpl({ as: IconComponent, ...props }: IconProps) {
return <IconComponent {...props} />;
}
cssInterop(IconImpl, {
className: {
target: 'style',
nativeStyleToProp: {
height: 'size',
width: 'size',
},
},
});
/**
* A wrapper component for Lucide icons with Nativewind `className` support via `cssInterop`.
*
* This component allows you to render any Lucide icon while applying utility classes
* using `nativewind`. It avoids the need to wrap or configure each icon individually.
*
* @component
* @example
* ```tsx
* import { ArrowRight } from 'lucide-react-native';
* import { Icon } from '@/registry/components/ui/icon';
*
* <Icon as={ArrowRight} className="text-red-500" size={16} />
* ```
*
* @param {LucideIcon} as - The Lucide icon component to render.
* @param {string} className - Utility classes to style the icon using Nativewind.
* @param {number} size - Icon size (defaults to 14).
* @param {...LucideProps} ...props - Additional Lucide icon props passed to the "as" icon.
*/
function Icon({ as: IconComponent, className, size = 14, ...props }: IconProps) {
return (
<IconImpl
as={IconComponent}
className={cn('text-foreground', className)}
size={size}
{...props}
/>
);
}
export { Icon };

89
components/ui/text.tsx Normal file
View File

@ -0,0 +1,89 @@
import { cn } from '@/lib/utils';
import * as Slot from '@rn-primitives/slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { Platform, Text as RNText, type Role } from 'react-native';
const textVariants = cva(
cn(
'text-base text-foreground',
Platform.select({
web: 'select-text',
})
),
{
variants: {
variant: {
default: '',
h1: cn(
'text-center text-4xl font-extrabold tracking-tight',
Platform.select({ web: 'scroll-m-20 text-balance' })
),
h2: cn(
'border-b border-border pb-2 text-3xl font-semibold tracking-tight',
Platform.select({ web: 'scroll-m-20 first:mt-0' })
),
h3: cn('text-2xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
h4: cn('text-xl font-semibold tracking-tight', Platform.select({ web: 'scroll-m-20' })),
p: 'mt-3 leading-7 sm:mt-6',
blockquote: 'mt-4 border-l-2 pl-3 italic sm:mt-6 sm:pl-6',
code: cn(
'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold'
),
lead: 'text-xl text-muted-foreground',
large: 'text-lg font-semibold',
small: 'text-sm font-medium leading-none',
muted: 'text-sm text-muted-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
type TextVariantProps = VariantProps<typeof textVariants>;
type TextVariant = NonNullable<TextVariantProps['variant']>;
const ROLE: Partial<Record<TextVariant, Role>> = {
h1: 'heading',
h2: 'heading',
h3: 'heading',
h4: 'heading',
blockquote: Platform.select({ web: 'blockquote' as Role }),
code: Platform.select({ web: 'code' as Role }),
};
const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
h1: '1',
h2: '2',
h3: '3',
h4: '4',
};
const TextClassContext = React.createContext<string | undefined>(undefined);
function Text({
className,
asChild = false,
variant = 'default',
...props
}: React.ComponentProps<typeof RNText> &
TextVariantProps &
React.RefAttributes<RNText> & {
asChild?: boolean;
}) {
const textClass = React.useContext(TextClassContext);
const Component = asChild ? Slot.Text : RNText;
return (
<Component
className={cn(textVariants({ variant }), textClass, className)}
role={variant ? ROLE[variant] : undefined}
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
{...props}
/>
);
}
export { Text, TextClassContext };

58
global.css Normal file
View File

@ -0,0 +1,58 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 63%;
--radius: 0.625rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark:root {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 70.9% 59.4%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 300 0% 45%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}

81
lib/theme.ts Normal file
View File

@ -0,0 +1,81 @@
import { DarkTheme, DefaultTheme, type Theme } from '@react-navigation/native';
export const THEME = {
light: {
background: 'hsl(0 0% 100%)',
foreground: 'hsl(0 0% 3.9%)',
card: 'hsl(0 0% 100%)',
cardForeground: 'hsl(0 0% 3.9%)',
popover: 'hsl(0 0% 100%)',
popoverForeground: 'hsl(0 0% 3.9%)',
primary: 'hsl(0 0% 9%)',
primaryForeground: 'hsl(0 0% 98%)',
secondary: 'hsl(0 0% 96.1%)',
secondaryForeground: 'hsl(0 0% 9%)',
muted: 'hsl(0 0% 96.1%)',
mutedForeground: 'hsl(0 0% 45.1%)',
accent: 'hsl(0 0% 96.1%)',
accentForeground: 'hsl(0 0% 9%)',
destructive: 'hsl(0 84.2% 60.2%)',
border: 'hsl(0 0% 89.8%)',
input: 'hsl(0 0% 89.8%)',
ring: 'hsl(0 0% 63%)',
radius: '0.625rem',
chart1: 'hsl(12 76% 61%)',
chart2: 'hsl(173 58% 39%)',
chart3: 'hsl(197 37% 24%)',
chart4: 'hsl(43 74% 66%)',
chart5: 'hsl(27 87% 67%)',
},
dark: {
background: 'hsl(0 0% 3.9%)',
foreground: 'hsl(0 0% 98%)',
card: 'hsl(0 0% 3.9%)',
cardForeground: 'hsl(0 0% 98%)',
popover: 'hsl(0 0% 3.9%)',
popoverForeground: 'hsl(0 0% 98%)',
primary: 'hsl(0 0% 98%)',
primaryForeground: 'hsl(0 0% 9%)',
secondary: 'hsl(0 0% 14.9%)',
secondaryForeground: 'hsl(0 0% 98%)',
muted: 'hsl(0 0% 14.9%)',
mutedForeground: 'hsl(0 0% 63.9%)',
accent: 'hsl(0 0% 14.9%)',
accentForeground: 'hsl(0 0% 98%)',
destructive: 'hsl(0 70.9% 59.4%)',
border: 'hsl(0 0% 14.9%)',
input: 'hsl(0 0% 14.9%)',
ring: 'hsl(300 0% 45%)',
radius: '0.625rem',
chart1: 'hsl(220 70% 50%)',
chart2: 'hsl(160 60% 45%)',
chart3: 'hsl(30 80% 55%)',
chart4: 'hsl(280 65% 60%)',
chart5: 'hsl(340 75% 55%)',
},
};
export const NAV_THEME: Record<'light' | 'dark', Theme> = {
light: {
...DefaultTheme,
colors: {
background: THEME.light.background,
border: THEME.light.border,
card: THEME.light.card,
notification: THEME.light.destructive,
primary: THEME.light.primary,
text: THEME.light.foreground,
},
},
dark: {
...DarkTheme,
colors: {
background: THEME.dark.background,
border: THEME.dark.border,
card: THEME.dark.card,
notification: THEME.dark.destructive,
primary: THEME.dark.primary,
text: THEME.dark.foreground,
},
},
};

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

6
metro.config.js Normal file
View File

@ -0,0 +1,6 @@
const { getDefaultConfig } = require('expo/metro-config');
const { withNativeWind } = require('nativewind/metro');
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: './global.css', inlineRem: 16 });

1
nativewind-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="nativewind/types" />

10415
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "mangamochi",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"dev": "expo start -c",
"android": "expo start -c --android",
"ios": "expo start -c --ios",
"web": "expo start -c --web",
"clean": "rm -rf .expo node_modules"
},
"dependencies": {
"@react-navigation/native": "^7.0.0",
"@rn-primitives/portal": "~1.4.0",
"@rn-primitives/slot": "^1.4.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"expo": "~55.0.9",
"expo-constants": "~55.0.9",
"expo-linking": "~55.0.9",
"expo-router": "~55.0.8",
"expo-splash-screen": "~55.0.13",
"expo-status-bar": "~55.0.4",
"expo-system-ui": "~55.0.11",
"lucide-react-native": "^0.545.0",
"nativewind": "^4.2.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.4",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-svg": "15.15.3",
"react-native-web": "^0.21.0",
"react-native-worklets": "0.7.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.14",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@types/react": "~19.2.10",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"typescript": "~5.9.2"
},
"private": true
}

73
tailwind.config.js Normal file
View File

@ -0,0 +1,73 @@
const { hairlineWidth } = require('nativewind/theme');
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
presets: [require('nativewind/preset')],
theme: {
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
borderWidth: {
hairline: hairlineWidth(),
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
future: {
hoverOnlyWhenSupported: true,
},
plugins: [require('tailwindcss-animate')],
};

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"]
}