尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Build React components once, run on web, iOS, and Android. A Turborepo + NativeWind monorepo template with shared UI.
New to monorepos? Follow the step-by-step guide that built this template.
Prerequisites: Node.js 18+, pnpm 10+, and optionally Xcode/Android Studio for mobile.
Other commands: pnpm build, pnpm lint, pnpm typecheck
Components in packages/ui/ are written once with React Native + NativeWind. On web, react-native-web renders them as HTML. On mobile, Expo renders them natively.
Both web (Next.js) and web-vite (Vite + TanStack Router) are included. Remove the one you don't need:檢視完整 README ↗Build React components once, run on web, iOS, and Android. A Turborepo + NativeWind monorepo template with shared UI.
New to monorepos? Follow the step-by-step guide that built this template.
Prerequisites: Node.js 18+, pnpm 10+, and optionally Xcode/Android Studio for mobile.
git clone https://github.com/gurselcakar/universal-react-monorepo.git
cd universal-react-monorepo
pnpm install
pnpm dev # Start all apps
Run individually:
pnpm --filter web dev # Next.js → localhost:3000
pnpm --filter web-vite dev # Vite → localhost:5173
pnpm --filter mobile dev # Expo Metro bundler
Other commands: pnpm build, pnpm lint, pnpm typecheck
| Layer | Technology |
|---|---|
| Web | Next.js 16 or Vite + TanStack Router |
| Mobile | Expo SDK 56 (React Native) |
| Shared UI | React Native + NativeWind |
| Build | Turborepo, pnpm workspaces, TypeScript |
Components in packages/ui/ are written once with React Native + NativeWind. On web, react-native-web renders them as HTML. On mobile, Expo renders them natively.
├── apps/
│ ├── mobile/ # Expo React Native app
│ ├── web/ # Next.js web app
│ └── web-vite/ # Vite web app (alternative)
├── packages/
│ └── ui/ # Shared component library
└── turbo.json # Turborepo config
Both web (Next.js) and web-vite (Vite + TanStack Router) are included. Remove the one you don't need:
Keep Next.js only:
rm -rf apps/web-vite
pnpm install
Keep Vite only:
rm -rf apps/web
mv apps/web-vite apps/web
# Update "name" in apps/web/package.json from "web-vite" to "web"
pnpm install
This repo runs NativeWind v5 (preview) with Tailwind CSS v4. Key differences from v4/v3:
tailwind.config.js)Tailwind v4 moves all configuration into CSS. tailwind.config.js is deleted. Theme tokens live in a @theme {} block inside the CSS entry file (e.g. global.css):
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css";
@import "nativewind/theme";
@theme {
--color-primary: hsl(var(--primary));
--font-sans: Inter-Regular, system-ui, sans-serif;
/* ... */
}
postcss.config.mjs with @tailwindcss/postcsspostcss.config.js with @tailwindcss/postcss@tailwindcss/vite plugin in vite.config.ts (no postcss needed)remapProps and cssInterop (v4) are replaced by a single styled function from nativewind (re-exported from react-native-css):
// v4 — removed
import { remapProps, cssInterop } from 'nativewind';
remapProps(Component, { className: 'style' });
cssInterop(Component, { className: 'style' });
// v5 — use this
import { styled } from 'nativewind';
styled(Component, { className: { target: 'style' } });
// With nativeStyleToProp (e.g. SVG components)
styled(Circle, {
className: { target: 'style', nativeStyleToProp: { fill: true, stroke: true } },
});
nativewind-env.d.ts now references react-native-css/types instead of nativewind/types:
/// <reference types="react-native-css/types" />
lightningcss pinThe root package.json pins lightningcss to 1.30.1 via pnpm.overrides to prevent Metro deserialization errors with global.css.
packages/ui/src/MaskedView.web.tsx is the web shim for @react-native-masked-view/masked-view.
mix-blend-mode: destination-in is unreliable across browsers due to compositing-context issues. The web implementation uses the proper browser masking API instead:
maskElement into a hidden off-screen div (outer wrapper at left: -100000, inner ref div at normal position so html-to-image clones it correctly)html-to-image (toPng, 2× pixel ratio)mask-image / -webkit-mask-image on the content layer// The mask's alpha channel controls visibility:
// opaque pixels → content visible
// transparent pixels → content hidden
<MaskedView
style={{ width: 200, height: 80 }}
maskElement={
<div style={{
width: 200, height: 80,
background: 'linear-gradient(to right, black 40%, transparent 100%)',
}} />
}
>
<div style={{ width: 200, height: 80, background: 'linear-gradient(135deg, #6366f1, #ec4899)' }} />
</MaskedView>
Important: mask element children must use explicit pixel dimensions (not
100%) to resolve correctly insideposition: absolutecontainers.
html-to-image is added to packages/ui for the rasterization step.
Based on betomoedano/keyboard-guide. The mobile app already ships react-native-keyboard-controller + KeyboardProvider (wired in apps/mobile/app/_layout.tsx). Prefer these patterns in order:
Do not pad or translate the container. Render an empty Animated.View below the input whose height tracks the keyboard frame-by-frame on the UI thread:
import { useKeyboardHandler } from 'react-native-keyboard-controller';
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
const PADDING_BOTTOM = 20;
const useGradualAnimation = () => {
const height = useSharedValue(PADDING_BOTTOM);
useKeyboardHandler(
{
onMove: (e) => {
'worklet';
height.value = Math.max(e.height, PADDING_BOTTOM);
},
onEnd: (e) => {
'worklet';
height.value = e.height;
},
},
[],
);
return { height };
};
// Screen:
const { height } = useGradualAnimation();
const fakeView = useAnimatedStyle(() => ({ height: Math.abs(height.value) }), []);
<FlatList inverted keyboardDismissMode="on-drag" ... />
<TextInput ... />
<Animated.View style={fakeView} /> {/* keyboard-synced spacer */}
Zero stutter — onMove worklets fire every frame of the keyboard animation. Use inverted lists so newest content stays pinned to the input.
For inputs holding paragraphs of text, make the input itself flex so it shrinks as the keyboard spacer grows. The flex column redistributes space automatically:
<View style={{ flex: 1, padding: 16 }}>
<TextInput
multiline
numberOfLines={8}
maxLength={280}
textAlignVertical="top" // Android: anchor text to top, not center
style={{
flex: 1, // ← input fills + shrinks with available space
padding: 16,
borderRadius: 16,
marginBottom: 16,
}}
/>
{/* fixed-height footer content stays visible above keyboard */}
<View style={{ flexDirection: 'row', gap: 16 }}>...</View>
<Animated.View style={keyboardPadding} /> {/* same fake-view spacer */}
</View>
<KeyboardToolbar
content={<Text>Custom toolbar content</Text>}
showArrows={false}
doneText="Close keyboard"
/>
Key points:
flex: 1 on the multiline TextInput — when the spacer grows, the input shrinks instead of being pushed off-screentextAlignVertical="top" — required on Android or paragraph text floats to vertical centermaxLength caps runaway content; numberOfLines sets the initial visual heightKeyboard.isVisible() ? Keyboard.dismiss() : inputRef.current?.focus()KeyboardAwareScrollView + KeyboardToolbarimport { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';
<KeyboardAwareScrollView bottomOffset={62} style={{ flex: 1, marginBottom: 62 }}>
{/* many TextInputs */}
</KeyboardAwareScrollView>
<KeyboardToolbar /> {/* prev / next / done navigation for free */}
Auto-scrolls the focused input above the keyboard. bottomOffset must reserve the toolbar height.
KeyboardAvoidingView (avoid if possible)<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined} // Android: let windowSoftInputMode handle it
>
<FlatList keyboardDismissMode="on-drag" keyboardShouldPersistTaps="always" />
<TextInput ... />
</KeyboardAvoidingView>
Only reacts to discrete show/hide events → visible stutter. Use patterns 1–3 instead.
apps/mobile/src/stickers/ui/StickerPickerSheet.tsx, apps/mobile/src/ticket/ui/TicketActionsBar.tsx, apps/mobile/src/stories-editor/components/text/TextEditor.tsx.
solito v5 is available in @dvnt/ui and provides a unified routing API for Next.js (web) and Expo Router (native).
next/link, next/navigation)expo-router)| Platform | Import Path | Usage |
|---|---|---|
| Web (Next.js) | next/link | Use Next.js Link directly |
| Native (Expo Router) | solito | Link, useRouter, hooks |
| All | solito | useRouter, useParams, useSearchParams, createParam |
Solito v5 wraps Expo Router for native. For web, use Next.js hooks directly:
// Native component (Expo Router via Solito)
import { Link, useRouter, useParams } from 'solito';
function EventDetail() {
const router = useRouter();
const { id } = useParams(); // Route params (/events/[id])
return (
<View>
<Text>Event {id}</Text>
<Link href="/events">Back to events</Link>
<Button onPress={() => router.push(`/events/${id}/edit`)}>
Edit
</Button>
</View>
);
}
// Web component (Next.js App Router)
import Link from 'next/link';
import { useRouter } from 'next/navigation';
function EventDetail() {
const router = useRouter();
return (
<div>
<Link href="/events">Back to events</Link>
<Button onClick={() => router.push(`/events/${id}/edit`)}>
Edit
</Button>
</div>
);
}
| Solito | Web (Next.js) | Native (Expo Router) |
|---|---|---|
useRouter() | next/navigation | expo-router |
useParams() | useParams() | useLocalSearchParams() |
useSearchParams() | useSearchParams() | useGlobalSearchParams() |
Link | next/link | expo-router/link |
usePathname() | usePathname() | usePathname() |
// Define typed params for a route
import { createParam } from 'solito';
const { useParam } = createParam<{ id: string; tab?: string }>();
function TabScreen() {
const [id] = useParam('id'); // string | undefined
const [tab] = useParam('tab'); // string | undefined
}
Solito provides linkTo and getLinkingConfig for deep-linking that works on both platforms:
import { getLinkingConfig } from 'solito';
// In your root layout or navigation container
const linking = getLinkingConfig({
prefixes: ['https://dvnt.app', 'dvnt://'],
config: {
screens: {
home: '',
events: 'events',
'events/[id]': 'events/:id',
},
},
});
See solito.dev for full App Directory docs.
/tanstack)apps/web/src/app/tanstack/page.tsx demonstrates four TanStack packages on a shared 500-row ticket dataset:
| Package | Demo |
|---|---|
@tanstack/react-table | Sortable + globally-filterable inventory table with progress bars |
@tanstack/react-virtual | 500-row list windowed to only visible DOM nodes |
@tanstack/react-form | useForm + form.Field + form.Subscribe with live preview |
@tanstack/react-pacer | useDebouncedCallback (400 ms) and useThrottledCallback (300 ms / 80 ms mouse) with an event log |
@tanstack/react-virtual (useVirtualizer) — DOM-native windowing@legendapp/list (Legend List) — RN-native high-performance listPick per platform; do not use Legend List for web-only virtualized views or react-virtual on native.
Web pages use top-tier semantic markup (the web equivalent of @expo/html-elements, which is native-only in apps/mobile):
<section aria-label> + <header> per showcase block, <article> for cards<search> + labelled type="search" inputs, <output> for computed values<table> with <caption>, scope="col", and aria-sort on sortable headers<form> with <fieldset>/<legend>, every input labelled via htmlFor/id<nav aria-label="Breadcrumb"> with aria-current="page"role="list"/role="listitem" on virtualized rows (valid with absolute-positioned sizer)<output aria-live="polite"> + <ul>/<time> for the event logapps/web/src/app/registry.tsx exports RNWStyleRegistry, which flushes react-native-web's atomic CSS into the SSR HTML via useServerInsertedHTML. Without this, RNW generates different class names on the server vs. client, causing React hydration mismatches.
// apps/web/src/app/registry.tsx
'use client'
import { useServerInsertedHTML } from 'next/navigation'
import { useRef } from 'react'
import { StyleSheet } from 'react-native'
export function RNWStyleRegistry({ children }: { children: React.ReactNode }) {
const isServerInserted = useRef(false)
useServerInsertedHTML(() => {
if (isServerInserted.current) return
isServerInserted.current = true
const sheet = StyleSheet.getSheet()
return <style dangerouslySetInnerHTML={{ __html: sheet.textContent }} id={sheet.id} />
})
return <>{children}</>
}
RNWStyleRegistry wraps ImageProvider/ApiProvider in apps/web/src/app/layout.tsx. The useRef guard prevents the Next.js double-invocation bug from inserting duplicate style tags.
Built by Gürsel Çakar. Check out my games: Hukora and Arithmego.