From 7851d0636d1e5b81756e2529f5680930004a446c Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 2 Jun 2025 16:30:53 +0530 Subject: [PATCH] add ai and todo example templates for native frontends (#293) --- .changeset/grumpy-bees-appear.md | 5 + .../project-generation/post-installation.ts | 10 +- .../project-generation/template-manager.ts | 30 ++ apps/cli/src/helpers/setup/examples-setup.ts | 4 +- apps/cli/src/prompts/examples.ts | 23 +- apps/cli/src/validation.ts | 18 - .../native/nativewind/app/(drawer)/ai.tsx.hbs | 155 ++++++++ .../ai/native/nativewind/polyfills.js | 25 ++ .../native/unistyles/app/(drawer)/ai.tsx.hbs | 279 ++++++++++++++ .../examples/ai/native/unistyles/polyfills.js | 25 ++ .../{todo.tsx.hbs => (drawer)/todos.tsx.hbs} | 0 .../unistyles/app/(drawer)/todos.tsx.hbs | 340 ++++++++++++++++++ .../app/(drawer)/(tabs)/_layout.tsx | 31 +- .../nativewind/app/(drawer)/(tabs)/index.tsx | 28 +- .../nativewind/app/(drawer)/(tabs)/two.tsx | 28 +- .../nativewind/app/(drawer)/_layout.tsx.hbs | 40 ++- .../nativewind/app/(drawer)/index.tsx.hbs | 73 ++-- .../native/nativewind/app/+not-found.tsx | 29 +- .../native/nativewind/app/_layout.tsx.hbs | 3 + .../frontend/native/nativewind/app/modal.tsx | 16 +- .../nativewind/components/container.tsx | 5 +- .../nativewind/components/header-button.tsx | 53 ++- .../nativewind/components/tabbar-icon.tsx | 13 +- .../frontend/native/nativewind/global.css | 47 ++- .../native/nativewind/lib/constants.ts | 16 +- .../{package.json => package.json.hbs} | 4 + .../native/nativewind/tailwind.config.js | 27 ++ .../unistyles/app/(drawer)/(tabs)/_layout.tsx | 13 +- .../unistyles/app/(drawer)/(tabs)/index.tsx | 42 ++- .../unistyles/app/(drawer)/(tabs)/two.tsx | 44 ++- .../(drawer)/{_layout.tsx => _layout.tsx.hbs} | 42 ++- .../unistyles/app/(drawer)/index.tsx.hbs | 185 ++++++---- .../native/unistyles/app/+not-found.tsx | 61 +++- .../native/unistyles/app/_layout.tsx.hbs | 15 +- .../frontend/native/unistyles/app/modal.tsx | 32 +- .../native/unistyles/components/container.tsx | 11 +- .../unistyles/components/header-button.tsx | 63 ++-- .../unistyles/components/tabbar-icon.tsx | 13 +- .../{package.json => package.json.hbs} | 6 +- .../frontend/native/unistyles/theme.ts | 99 ++++- .../frontend/native/unistyles/unistyles.ts | 8 +- .../app/(home)/_components/stack-builder.tsx | 181 +++------- 42 files changed, 1606 insertions(+), 536 deletions(-) create mode 100644 .changeset/grumpy-bees-appear.md create mode 100644 apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs create mode 100644 apps/cli/templates/examples/ai/native/nativewind/polyfills.js create mode 100644 apps/cli/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs create mode 100644 apps/cli/templates/examples/ai/native/unistyles/polyfills.js rename apps/cli/templates/examples/todo/native/nativewind/app/{todo.tsx.hbs => (drawer)/todos.tsx.hbs} (100%) create mode 100644 apps/cli/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs rename apps/cli/templates/frontend/native/nativewind/{package.json => package.json.hbs} (90%) rename apps/cli/templates/frontend/native/unistyles/app/(drawer)/{_layout.tsx => _layout.tsx.hbs} (57%) rename apps/cli/templates/frontend/native/unistyles/{package.json => package.json.hbs} (88%) diff --git a/.changeset/grumpy-bees-appear.md b/.changeset/grumpy-bees-appear.md new file mode 100644 index 0000000..90ff77c --- /dev/null +++ b/.changeset/grumpy-bees-appear.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add AI and todo example templates for nativewind and unistyles diff --git a/apps/cli/src/helpers/project-generation/post-installation.ts b/apps/cli/src/helpers/project-generation/post-installation.ts index 42b82c1..4534286 100644 --- a/apps/cli/src/helpers/project-generation/post-installation.ts +++ b/apps/cli/src/helpers/project-generation/post-installation.ts @@ -145,9 +145,17 @@ function getNativeInstructions(isConvex: boolean): string { ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address"; - return `${pc.yellow( + let instructions = `${pc.yellow( "NOTE:", )} For Expo connectivity issues, update apps/native/${envFileName} \nwith ${ipNote}:\n${`${envVar}=${exampleUrl}`}\n`; + + if (isConvex) { + instructions += `\n${pc.yellow( + "IMPORTANT:", + )} When using local development with Convex and native apps, ensure you use your local IP address \ninstead of localhost or 127.0.0.1 for proper connectivity.\n`; + } + + return instructions; } function getLintingInstructions(runCmd?: string): string { diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 6d697d5..6edaf6b 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -599,6 +599,8 @@ export async function setupExamplesTemplate( const serverAppDirExists = await fs.pathExists(serverAppDir); const webAppDirExists = await fs.pathExists(webAppDir); + const nativeAppDir = path.join(projectDir, "apps/native"); + const nativeAppDirExists = await fs.pathExists(nativeAppDir); const hasReactWeb = context.frontend.some((f) => ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), @@ -758,6 +760,34 @@ export async function setupExamplesTemplate( } } } + + if (nativeAppDirExists) { + const hasNativeWind = context.frontend.includes("native-nativewind"); + const hasUnistyles = context.frontend.includes("native-unistyles"); + + if (hasNativeWind || hasUnistyles) { + let nativeFramework = ""; + if (hasNativeWind) { + nativeFramework = "nativewind"; + } else if (hasUnistyles) { + nativeFramework = "unistyles"; + } + + const exampleNativeSrc = path.join( + exampleBaseDir, + `native/${nativeFramework}`, + ); + if (await fs.pathExists(exampleNativeSrc)) { + await processAndCopyFiles( + "**/*", + exampleNativeSrc, + nativeAppDir, + context, + false, + ); + } + } + } } } diff --git a/apps/cli/src/helpers/setup/examples-setup.ts b/apps/cli/src/helpers/setup/examples-setup.ts index 33da140..97f0b80 100644 --- a/apps/cli/src/helpers/setup/examples-setup.ts +++ b/apps/cli/src/helpers/setup/examples-setup.ts @@ -28,7 +28,9 @@ export async function setupExamples(config: ProjectConfig): Promise { frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next") || - frontend.includes("tanstack-start"); + frontend.includes("tanstack-start") || + frontend.includes("native-nativewind") || + frontend.includes("native-unistyles"); if (clientDirExists) { const dependencies: AvailableDependencies[] = ["ai"]; diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index d562ea5..3d39ada 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -25,30 +25,9 @@ export async function getExamplesChoice( if (database === "none") return []; - const onlyNative = - frontends && - frontends.length === 1 && - (frontends[0] === "native-nativewind" || - frontends[0] === "native-unistyles"); - if (onlyNative) { - return []; - } - - const hasWebFrontend = - frontends?.some((f) => - [ - "react-router", - "tanstack-router", - "tanstack-start", - "next", - "nuxt", - "svelte", - "solid", - ].includes(f), - ) ?? false; const noFrontendSelected = !frontends || frontends.length === 0; - if (!hasWebFrontend && !noFrontendSelected) return []; + if (noFrontendSelected) return []; let response: Examples[] | symbol = []; const options: { value: Examples; label: string; hint: string }[] = [ diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index d54d624..51942c4 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -467,24 +467,6 @@ export function validateConfigCompatibility( config.addons = [...new Set(config.addons)]; } - const onlyNativeFrontend = - effectiveFrontend && - effectiveFrontend.length === 1 && - (effectiveFrontend[0] === "native-nativewind" || - effectiveFrontend[0] === "native-unistyles"); - - if ( - onlyNativeFrontend && - config.examples && - config.examples.length > 0 && - !config.examples.includes("none") - ) { - consola.fatal( - "Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.", - ); - process.exit(1); - } - if ( config.examples && config.examples.length > 0 && diff --git a/apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs b/apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs new file mode 100644 index 0000000..08d84df --- /dev/null +++ b/apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs @@ -0,0 +1,155 @@ +import { useRef, useEffect } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; +import { useChat } from "@ai-sdk/react"; +import { fetch as expoFetch } from "expo/fetch"; +import { Ionicons } from "@expo/vector-icons"; +import { Container } from "@/components/container"; + +// Utility function to generate API URLs +const generateAPIUrl = (relativePath: string) => { + const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; + if (!serverUrl) { + throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined"); + } + + const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`; + return serverUrl.concat(path); +}; + +export default function AIScreen() { + const { messages, input, handleInputChange, handleSubmit, error } = useChat({ + fetch: expoFetch as unknown as typeof globalThis.fetch, + api: generateAPIUrl('/ai'), + onError: error => console.error(error, 'AI Chat Error'), + maxSteps: 5, + }); + + const scrollViewRef = useRef(null); + + useEffect(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, [messages]); + + const onSubmit = () => { + if (input.trim()) { + handleSubmit(); + } + }; + + if (error) { + return ( + + + + Error: {error.message} + + + Please check your connection and try again. + + + + ); + } + + return ( + + + + + + AI Chat + + + Chat with our AI assistant + + + + + {messages.length === 0 ? ( + + + Ask me anything to get started! + + + ) : ( + + {messages.map((message) => ( + + + {message.role === "user" ? "You" : "AI Assistant"} + + + {message.content} + + + ))} + + )} + + + + + + handleInputChange({ + ...e, + target: { + ...e.target, + value: e.nativeEvent.text, + }, + } as unknown as React.ChangeEvent) + } + placeholder="Type your message..." + placeholderTextColor="#6b7280" + className="flex-1 border border-border rounded-md px-3 py-2 text-foreground bg-background min-h-[40px] max-h-[120px]" + onSubmitEditing={(e) => { + handleSubmit(e); + e.preventDefault(); + }} + autoFocus={true} + /> + + + + + + + + + ); +} \ No newline at end of file diff --git a/apps/cli/templates/examples/ai/native/nativewind/polyfills.js b/apps/cli/templates/examples/ai/native/nativewind/polyfills.js new file mode 100644 index 0000000..8e2e56f --- /dev/null +++ b/apps/cli/templates/examples/ai/native/nativewind/polyfills.js @@ -0,0 +1,25 @@ +import structuredClone from "@ungap/structured-clone"; +import { Platform } from "react-native"; + +if (Platform.OS !== "web") { + const setupPolyfills = async () => { + const { polyfillGlobal } = await import( + "react-native/Libraries/Utilities/PolyfillFunctions" + ); + + const { TextEncoderStream, TextDecoderStream } = await import( + "@stardazed/streams-text-encoding" + ); + + if (!("structuredClone" in global)) { + polyfillGlobal("structuredClone", () => structuredClone); + } + + polyfillGlobal("TextEncoderStream", () => TextEncoderStream); + polyfillGlobal("TextDecoderStream", () => TextDecoderStream); + }; + + setupPolyfills(); +} + +export {}; diff --git a/apps/cli/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs b/apps/cli/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs new file mode 100644 index 0000000..c59e844 --- /dev/null +++ b/apps/cli/templates/examples/ai/native/unistyles/app/(drawer)/ai.tsx.hbs @@ -0,0 +1,279 @@ +import { useRef, useEffect } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + KeyboardAvoidingView, + Platform, +} from "react-native"; +import { useChat } from "@ai-sdk/react"; +import { fetch as expoFetch } from "expo/fetch"; +import { Ionicons } from "@expo/vector-icons"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { Container } from "@/components/container"; + +const generateAPIUrl = (relativePath: string) => { + const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; + if (!serverUrl) { + throw new Error( + "EXPO_PUBLIC_SERVER_URL environment variable is not defined", + ); + } + + const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`; + return serverUrl.concat(path); +}; + +export default function AIScreen() { + const { theme } = useUnistyles(); + const { messages, input, handleInputChange, handleSubmit, error } = useChat({ + fetch: expoFetch as unknown as typeof globalThis.fetch, + api: generateAPIUrl("/ai"), + onError: (error) => console.error(error, "AI Chat Error"), + maxSteps: 5, + }); + + const scrollViewRef = useRef(null); + + useEffect(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, [messages]); + + const onSubmit = () => { + if (input.trim()) { + handleSubmit(); + } + }; + + if (error) { + return ( + + + Error: {error.message} + + Please check your connection and try again. + + + + ); + } + + return ( + + + + + AI Chat + + Chat with our AI assistant + + + + + {messages.length === 0 ? ( + + + Ask me anything to get started! + + + ) : ( + + {messages.map((message) => ( + + + {message.role === "user" ? "You" : "AI Assistant"} + + {message.content} + + ))} + + )} + + + + + + handleInputChange({ + ...e, + target: { + ...e.target, + value: e.nativeEvent.text, + }, + } as unknown as React.ChangeEvent) + } + placeholder="Type your message..." + placeholderTextColor={theme.colors.border} + style={styles.textInput} + onSubmitEditing={(e) => { + handleSubmit(e); + e.preventDefault(); + }} + autoFocus={true} + /> + + + + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + content: { + flex: 1, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + }, + errorContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: theme.spacing.md, + }, + errorText: { + color: theme.colors.destructive, + textAlign: "center", + fontSize: 18, + marginBottom: theme.spacing.md, + }, + errorSubtext: { + color: theme.colors.typography, + textAlign: "center", + fontSize: 16, + }, + header: { + marginBottom: theme.spacing.lg, + }, + headerTitle: { + fontSize: 28, + fontWeight: "bold", + color: theme.colors.typography, + marginBottom: theme.spacing.sm, + }, + headerSubtitle: { + fontSize: 16, + color: theme.colors.typography, + }, + messagesContainer: { + flex: 1, + marginBottom: theme.spacing.md, + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + emptyText: { + textAlign: "center", + color: theme.colors.typography, + fontSize: 18, + }, + messagesWrapper: { + gap: theme.spacing.md, + }, + messageContainer: { + padding: theme.spacing.md, + borderRadius: 8, + }, + userMessage: { + backgroundColor: theme.colors.primary + "20", + marginLeft: theme.spacing.xl, + alignSelf: "flex-end", + }, + assistantMessage: { + backgroundColor: theme.colors.background, + marginRight: theme.spacing.xl, + borderWidth: 1, + borderColor: theme.colors.border, + }, + messageRole: { + fontSize: 14, + fontWeight: "600", + marginBottom: theme.spacing.sm, + color: theme.colors.typography, + }, + messageContent: { + color: theme.colors.typography, + lineHeight: 20, + }, + toolInvocations: { + fontSize: 12, + color: theme.colors.typography, + fontFamily: "monospace", + backgroundColor: theme.colors.border + "40", + padding: theme.spacing.sm, + borderRadius: 4, + marginTop: theme.spacing.sm, + }, + inputSection: { + borderTopWidth: 1, + borderTopColor: theme.colors.border, + paddingTop: theme.spacing.md, + }, + inputContainer: { + flexDirection: "row", + alignItems: "flex-end", + gap: theme.spacing.sm, + }, + textInput: { + flex: 1, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 8, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + color: theme.colors.typography, + backgroundColor: theme.colors.background, + fontSize: 16, + minHeight: 40, + maxHeight: 120, + }, + sendButton: { + backgroundColor: theme.colors.primary, + padding: theme.spacing.sm, + borderRadius: 8, + justifyContent: "center", + alignItems: "center", + }, + sendButtonDisabled: { + backgroundColor: theme.colors.border, + }, +})); diff --git a/apps/cli/templates/examples/ai/native/unistyles/polyfills.js b/apps/cli/templates/examples/ai/native/unistyles/polyfills.js new file mode 100644 index 0000000..8e2e56f --- /dev/null +++ b/apps/cli/templates/examples/ai/native/unistyles/polyfills.js @@ -0,0 +1,25 @@ +import structuredClone from "@ungap/structured-clone"; +import { Platform } from "react-native"; + +if (Platform.OS !== "web") { + const setupPolyfills = async () => { + const { polyfillGlobal } = await import( + "react-native/Libraries/Utilities/PolyfillFunctions" + ); + + const { TextEncoderStream, TextDecoderStream } = await import( + "@stardazed/streams-text-encoding" + ); + + if (!("structuredClone" in global)) { + polyfillGlobal("structuredClone", () => structuredClone); + } + + polyfillGlobal("TextEncoderStream", () => TextEncoderStream); + polyfillGlobal("TextDecoderStream", () => TextDecoderStream); + }; + + setupPolyfills(); +} + +export {}; diff --git a/apps/cli/templates/examples/todo/native/nativewind/app/todo.tsx.hbs b/apps/cli/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs similarity index 100% rename from apps/cli/templates/examples/todo/native/nativewind/app/todo.tsx.hbs rename to apps/cli/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs diff --git a/apps/cli/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs b/apps/cli/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs new file mode 100644 index 0000000..9c5e631 --- /dev/null +++ b/apps/cli/templates/examples/todo/native/unistyles/app/(drawer)/todos.tsx.hbs @@ -0,0 +1,340 @@ +import { useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + ActivityIndicator, + Alert, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; + +{{#if (eq backend "convex")}} +import { useMutation, useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; +{{else}} +import { useMutation, useQuery } from "@tanstack/react-query"; +{{/if}} + +import { Container } from "@/components/container"; +{{#unless (eq backend "convex")}} +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} +import { trpc } from "@/utils/trpc"; +{{/if}} +{{/unless}} + +export default function TodosScreen() { + const [newTodoText, setNewTodoText] = useState(""); + const { theme } = useUnistyles(); + + {{#if (eq backend "convex")}} + const todos = useQuery(api.todos.getAll); + const createTodoMutation = useMutation(api.todos.create); + const toggleTodoMutation = useMutation(api.todos.toggle); + const deleteTodoMutation = useMutation(api.todos.deleteTodo); + + const handleAddTodo = async () => { + const text = newTodoText.trim(); + if (!text) return; + await createTodoMutation({ text }); + setNewTodoText(""); + }; + + const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => { + toggleTodoMutation({ id, completed: !currentCompleted }); + }; + + const handleDeleteTodo = (id: Id<"todos">) => { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteTodoMutation({ id }), + }, + ]); + }; + {{else}} + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }) + ); + const toggleMutation = useMutation( + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + const deleteMutation = useMutation( + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + {{/if}} + {{#if (eq api "trpc")}} + const todos = useQuery(trpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + trpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }) + ); + const toggleMutation = useMutation( + trpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + const deleteMutation = useMutation( + trpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + {{/if}} + + const handleAddTodo = () => { + if (newTodoText.trim()) { + createMutation.mutate({ text: newTodoText }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteMutation.mutate({ id }), + }, + ]); + }; + {{/if}} + + const isLoading = {{#if (eq backend "convex")}}!todos{{else}}todos.isLoading{{/if}}; + const isCreating = {{#if (eq backend "convex")}}false{{else}}createMutation.isPending{{/if}}; + const primaryButtonTextColor = theme.colors.background; + + return ( + + + + Todo List + + Manage your tasks efficiently + + + + + + {isCreating ? ( + + ) : ( + + )} + + + + + {isLoading && ( + + + Loading todos... + + )} + + {{#if (eq backend "convex")}} + {todos && todos.length === 0 && !isLoading && ( + No todos yet. Add one! + )} + {todos?.map((todo) => ( + + handleToggleTodo(todo._id, todo.completed)} + style={styles.todoContent} + > + + + {todo.text} + + + handleDeleteTodo(todo._id)}> + + + + ))} + {{else}} + {todos.data && todos.data.length === 0 && !isLoading && ( + No todos yet. Add one! + )} + {todos.data?.map((todo: { id: number; text: string; completed: boolean }) => ( + + handleToggleTodo(todo.id, todo.completed)} + style={styles.todoContent} + > + + + {todo.text} + + + handleDeleteTodo(todo.id)}> + + + + ))} + {{/if}} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + scrollView: { + flex: 1, + }, + headerContainer: { + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.lg, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + backgroundColor: theme.colors.background, + }, + headerTitle: { + fontSize: 28, + fontWeight: "bold", + color: theme.colors.typography, + marginBottom: theme.spacing.sm, + }, + headerSubtitle: { + fontSize: 16, + color: theme.colors.typography, + marginBottom: theme.spacing.md, + }, + inputContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: theme.spacing.md, + }, + textInput: { + flex: 1, + borderWidth: 1, + borderColor: theme.colors.border, + borderRadius: 8, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + color: theme.colors.typography, + backgroundColor: theme.colors.background, + marginRight: theme.spacing.sm, + fontSize: 16, + }, + addButton: { + backgroundColor: theme.colors.primary, + padding: theme.spacing.sm, + borderRadius: 8, + justifyContent: "center", + alignItems: "center", + }, + addButtonDisabled: { + backgroundColor: theme.colors.border, + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: theme.spacing.lg, + }, + loadingText: { + marginTop: theme.spacing.sm, + fontSize: 16, + color: theme.colors.typography, + }, + emptyText: { + textAlign: "center", + marginTop: theme.spacing.xl, + fontSize: 16, + color: theme.colors.typography, + }, + todoItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + backgroundColor: theme.colors.background, + }, + todoContent: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + checkbox: { + marginRight: theme.spacing.md, + }, + todoText: { + fontSize: 16, + color: theme.colors.typography, + flex: 1, + }, + todoTextCompleted: { + textDecorationLine: "line-through", + color: theme.colors.border, + }, +})); \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx index 852bbc9..617283c 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx +++ b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx @@ -1,27 +1,44 @@ +import { TabBarIcon } from "@/components/tabbar-icon"; +import { useColorScheme } from "@/lib/use-color-scheme"; import { Tabs } from "expo-router"; -import { TabBarIcon } from "@/components/tabbar-icon"; - export default function TabLayout() { + const { isDarkColorScheme } = useColorScheme(); + return ( , + title: "Home", + tabBarIcon: ({ color }) => , }} /> , + title: "Explore", + tabBarIcon: ({ color }) => ( + + ), }} /> diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx index 3d5b8ba..24a0512 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx +++ b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx @@ -1,17 +1,19 @@ import { Container } from "@/components/container"; -import { Text, View } from "react-native"; +import { ScrollView, Text, View } from "react-native"; export default function TabOne() { - return ( - - - - Tab One - - - This is the first tab of the application. - - - - ); + return ( + + + + + Tab One + + + Explore the first section of your app + + + + + ); } diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx index 5e13d51..1736606 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx +++ b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx @@ -1,17 +1,19 @@ import { Container } from "@/components/container"; -import { Text, View } from "react-native"; +import { ScrollView, Text, View } from "react-native"; export default function TabTwo() { - return ( - - - - Tab Two - - - This is the second tab of the application. - - - - ); + return ( + + + + + Tab Two + + + Discover more features and content + + + + + ); } diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs index 176a46b..a643ad0 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs +++ b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs @@ -17,18 +17,6 @@ const DrawerLayout = () => { ), }} /> - {{#if (includes examples "todo")}} - ( - - ), - }} - /> - {{/if}} { ), }} /> + {{#if (includes examples "todo")}} + ( + + ), + }} + /> + {{/if}} + {{#if (includes examples "ai")}} + ( + + ), + }} + /> + {{/if}} ); }; diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs index a290a6f..4db82d5 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs +++ b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs @@ -26,52 +26,65 @@ export default function Home() { return ( - - - BETTER T STACK - - - - API Status + + + BETTER T STACK + + {{#if (eq backend "convex")}} - + - + + + Convex + + {healthCheck === undefined - ? "Checking..." + ? "Checking connection..." : healthCheck === "OK" - ? "Connected" - : "Error"} - + ? "All systems operational" + : "Service unavailable"} + + {{else}} {{#unless (eq api "none")}} - + - - {{#if (eq api "orpc")}} + + + {{#if (eq api "orpc")}} + ORPC + {{/if}} + {{#if (eq api "trpc")}} + TRPC + {{/if}} + + + {{#if (eq api "orpc")}} {healthCheck.isLoading - ? "Checking..." + ? "Checking connection..." : healthCheck.data - ? "Connected" - : "Disconnected"} - {{/if}} - {{#if (eq api "trpc")}} + ? "All systems operational" + : "Service unavailable"} + {{/if}} + {{#if (eq api "trpc")}} {healthCheck.isLoading - ? "Checking..." + ? "Checking connection..." : healthCheck.data - ? "Connected" - : "Disconnected"} - {{/if}} - + ? "All systems operational" + : "Service unavailable"} + {{/if}} + + {{/unless}} {{/if}} diff --git a/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx b/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx index 3daf273..50cd5ee 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx +++ b/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx @@ -1,17 +1,28 @@ -import { Link, Stack } from 'expo-router'; -import { Text } from 'react-native'; - -import { Container } from '@/components/container'; +import { Container } from "@/components/container"; +import { Link, Stack } from "expo-router"; +import { Text, View } from "react-native"; export default function NotFoundScreen() { return ( <> - + - This screen doesn't exist. - - Go to home screen! - + + + 🤔 + + Page Not Found + + + Sorry, the page you're looking for doesn't exist. + + + + Go to Home + + + + ); diff --git a/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs index ce86f64..132328a 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs +++ b/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs @@ -1,3 +1,6 @@ +{{#if (includes examples "ai")}} +import "@/polyfills"; +{{/if}} {{#if (eq backend "convex")}} import { ConvexProvider, ConvexReactClient } from "convex/react"; {{else}} diff --git a/apps/cli/templates/frontend/native/nativewind/app/modal.tsx b/apps/cli/templates/frontend/native/nativewind/app/modal.tsx index 9a6f4c5..7fdcaea 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/modal.tsx +++ b/apps/cli/templates/frontend/native/nativewind/app/modal.tsx @@ -2,11 +2,13 @@ import { Container } from "@/components/container"; import { Text, View } from "react-native"; export default function Modal() { - return ( - - - Modal View - - - ); + return ( + + + + Modal + + + + ); } diff --git a/apps/cli/templates/frontend/native/nativewind/components/container.tsx b/apps/cli/templates/frontend/native/nativewind/components/container.tsx index 21c7904..d1d5798 100644 --- a/apps/cli/templates/frontend/native/nativewind/components/container.tsx +++ b/apps/cli/templates/frontend/native/nativewind/components/container.tsx @@ -1,9 +1,8 @@ +import React from "react"; import { SafeAreaView } from "react-native"; export const Container = ({ children }: { children: React.ReactNode }) => { return ( - - {children} - + {children} ); }; diff --git a/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx b/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx index c998f53..51dcf38 100644 --- a/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx +++ b/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx @@ -1,31 +1,26 @@ -import FontAwesome from '@expo/vector-icons/FontAwesome'; -import { forwardRef } from 'react'; -import { Pressable, StyleSheet } from 'react-native'; +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import { forwardRef } from "react"; +import { Pressable } from "react-native"; -export const HeaderButton = forwardRef void }>( - ({ onPress }, ref) => { - return ( - - {({ pressed }) => ( - - )} - - ); - } -); - -export const styles = StyleSheet.create({ - headerRight: { - marginRight: 15, - }, +export const HeaderButton = forwardRef< + typeof Pressable, + { onPress?: () => void } +>(({ onPress }, ref) => { + return ( + + {({ pressed }) => ( + + )} + + ); }); diff --git a/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx b/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx index e75c9d3..3ea0888 100644 --- a/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx +++ b/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx @@ -1,15 +1,8 @@ -import FontAwesome from '@expo/vector-icons/FontAwesome'; -import { StyleSheet } from 'react-native'; +import FontAwesome from "@expo/vector-icons/FontAwesome"; export const TabBarIcon = (props: { - name: React.ComponentProps['name']; + name: React.ComponentProps["name"]; color: string; }) => { - return ; + return ; }; - -export const styles = StyleSheet.create({ - tabBarIcon: { - marginBottom: -3, - }, -}); diff --git a/apps/cli/templates/frontend/native/nativewind/global.css b/apps/cli/templates/frontend/native/nativewind/global.css index c0c1563..f658ca1 100644 --- a/apps/cli/templates/frontend/native/nativewind/global.css +++ b/apps/cli/templates/frontend/native/nativewind/global.css @@ -5,21 +5,46 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 84% 4.9%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 40%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 84% 4.9%; --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 8px; } .dark:root { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 84% 4.9%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 70%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; --destructive: 0 72% 51%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 94.1%; } } diff --git a/apps/cli/templates/frontend/native/nativewind/lib/constants.ts b/apps/cli/templates/frontend/native/nativewind/lib/constants.ts index b1dd3db..8e7ca11 100644 --- a/apps/cli/templates/frontend/native/nativewind/lib/constants.ts +++ b/apps/cli/templates/frontend/native/nativewind/lib/constants.ts @@ -1,18 +1,18 @@ export const NAV_THEME = { light: { background: "hsl(0 0% 100%)", - border: "hsl(240 5.9% 90%)", + border: "hsl(220 13% 91%)", card: "hsl(0 0% 100%)", notification: "hsl(0 84.2% 60.2%)", - primary: "hsl(240 5.9% 10%)", - text: "hsl(240 10% 3.9%)", + primary: "hsl(221.2 83.2% 53.3%)", + text: "hsl(222.2 84% 4.9%)", }, dark: { - background: "hsl(240 10% 3.9%)", - border: "hsl(240 3.7% 15.9%)", - card: "hsl(240 10% 3.9%)", + background: "hsl(222.2 84% 4.9%)", + border: "hsl(217.2 32.6% 17.5%)", + card: "hsl(222.2 84% 4.9%)", notification: "hsl(0 72% 51%)", - primary: "hsl(0 0% 98%)", - text: "hsl(0 0% 98%)", + primary: "hsl(217.2 91.2% 59.8%)", + text: "hsl(210 40% 98%)", }, }; diff --git a/apps/cli/templates/frontend/native/nativewind/package.json b/apps/cli/templates/frontend/native/nativewind/package.json.hbs similarity index 90% rename from apps/cli/templates/frontend/native/nativewind/package.json rename to apps/cli/templates/frontend/native/nativewind/package.json.hbs index c2d5b75..66c473e 100644 --- a/apps/cli/templates/frontend/native/nativewind/package.json +++ b/apps/cli/templates/frontend/native/nativewind/package.json.hbs @@ -16,6 +16,10 @@ "@react-navigation/native": "^7.0.14", "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.69.2", + {{#if (includes examples "ai")}} + "@stardazed/streams-text-encoding": "^1.0.2", + "@ungap/structured-clone": "^1.3.0", + {{/if}} "expo": "^53.0.4", "expo-constants": "~17.1.4", "expo-linking": "~7.1.4", diff --git a/apps/cli/templates/frontend/native/nativewind/tailwind.config.js b/apps/cli/templates/frontend/native/nativewind/tailwind.config.js index fd0b3c7..92da9bf 100644 --- a/apps/cli/templates/frontend/native/nativewind/tailwind.config.js +++ b/apps/cli/templates/frontend/native/nativewind/tailwind.config.js @@ -11,6 +11,14 @@ module.exports = { colors: { background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", @@ -19,9 +27,28 @@ module.exports = { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, destructive: { DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + radius: "var(--radius)", + }, + borderRadius: { + xl: "calc(var(--radius) + 4px)", + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", }, borderWidth: { hairline: hairlineWidth(), diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx index e4a5e2b..86f1e41 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx @@ -10,23 +10,28 @@ export default function TabLayout() { , + title: "Home", + tabBarIcon: ({ color }) => , }} /> , + title: "Explore", + tabBarIcon: ({ color }) => ( + + ), }} /> diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx index 52f73f4..e430dbc 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx @@ -1,29 +1,37 @@ -import { Stack } from "expo-router"; -import { StyleSheet } from "react-native-unistyles"; import { Container } from "@/components/container"; -import { Text, View } from "react-native"; +import { ScrollView, Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; export default function Home() { return ( - <> - - - - Tab One + + + + Tab One + + Explore the first section of your app + - - + + ); } const styles = StyleSheet.create((theme) => ({ - text: { - color: theme.colors.typography, - }, container: { - flex: 1, - paddingBottom: 100, - justifyContent: "center", - alignItems: "center", + padding: theme.spacing.lg, + }, + headerSection: { + paddingVertical: theme.spacing.xl, + }, + title: { + fontSize: theme.fontSize["3xl"], + fontWeight: "bold", + color: theme.colors.foreground, + marginBottom: theme.spacing.sm, + }, + subtitle: { + fontSize: theme.fontSize.lg, + color: theme.colors.mutedForeground, }, })); diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx index b75640e..2d1a922 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx @@ -1,29 +1,37 @@ -import { Stack } from "expo-router"; -import { StyleSheet } from "react-native-unistyles"; import { Container } from "@/components/container"; -import { Text, View } from "react-native"; +import { ScrollView, Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; -export default function Home() { +export default function TabTwo() { return ( - <> - - - - Tab Two + + + + Tab Two + + Discover more features and content + - - + + ); } const styles = StyleSheet.create((theme) => ({ - text: { - color: theme.colors.typography, - }, container: { - flex: 1, - paddingBottom: 100, - justifyContent: "center", - alignItems: "center", + padding: theme.spacing.lg, + }, + headerSection: { + paddingVertical: theme.spacing.xl, + }, + title: { + fontSize: theme.fontSize["3xl"], + fontWeight: "bold", + color: theme.colors.foreground, + marginBottom: theme.spacing.sm, + }, + subtitle: { + fontSize: theme.fontSize.lg, + color: theme.colors.mutedForeground, }, })); diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx.hbs similarity index 57% rename from apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx rename to apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx.hbs index fc0d630..96c8601 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx.hbs @@ -10,26 +10,26 @@ const DrawerLayout = () => { return ( ( @@ -39,7 +39,7 @@ const DrawerLayout = () => { /> ( @@ -52,6 +52,34 @@ const DrawerLayout = () => { ), }} /> + {{#if (includes examples "todo")}} + ( + + ), + }} + /> + {{/if}} + {{#if (includes examples "ai")}} + ( + + ), + }} + /> + {{/if}} ); }; diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs index 6dc0dda..d356ee7 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs +++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs @@ -28,56 +28,79 @@ export default function Home() { return ( - - BETTER T STACK + + + BETTER T STACK + + + + System Status + + LIVE + + - - API Status {{#if (eq backend "convex")}} - + - - {healthCheck === undefined - ? "Checking..." - : healthCheck === "OK" - ? "Connected" - : "Error"} - + + Convex + + {healthCheck === undefined + ? "Checking connection..." + : healthCheck === "OK" + ? "All systems operational" + : "Service unavailable"} + + {{else}} {{#unless (eq api "none")}} - + - - {{#if (eq api "orpc")}} - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - {{/if}} - {{#if (eq api "trpc")}} - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected" - : "Disconnected"} - {{/if}} - + + + {{#if (eq api "orpc")}} + ORPC + {{/if}} + {{#if (eq api "trpc")}} + TRPC + {{/if}} + + + {{#if (eq api "orpc")}} + {healthCheck.isLoading + ? "Checking connection..." + : healthCheck.data + ? "All systems operational" + : "Service unavailable"} + {{/if}} + {{#if (eq api "trpc")}} + {healthCheck.isLoading + ? "Checking connection..." + : healthCheck.data + ? "All systems operational" + : "Service unavailable"} + {{/if}} + + {{/unless}} {{/if}} @@ -88,44 +111,84 @@ export default function Home() { } const styles = StyleSheet.create((theme) => ({ - pageContainer: { - paddingHorizontal: 8, + container: { + paddingHorizontal: theme.spacing.md, }, - headerTitle: { - color: theme?.colors?.typography, - fontSize: 30, + heroSection: { + paddingVertical: theme.spacing.xl, + }, + heroTitle: { + fontSize: theme.fontSize["4xl"], fontWeight: "bold", - marginBottom: 16, + color: theme.colors.foreground, + marginBottom: theme.spacing.sm, }, - apiStatusCard: { - marginBottom: 24, - borderRadius: 8, + heroSubtitle: { + fontSize: theme.fontSize.lg, + color: theme.colors.mutedForeground, + lineHeight: 28, + }, + statusCard: { + backgroundColor: theme.colors.card, borderWidth: 1, - borderColor: theme?.colors?.border, - padding: 16, + borderColor: theme.colors.border, + borderRadius: theme.borderRadius.xl, + padding: theme.spacing.lg, + marginBottom: theme.spacing.lg, + shadowColor: "#000", + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 3, + elevation: 2, }, - cardTitle: { - marginBottom: 12, - fontWeight: "500", - color: theme?.colors?.typography, - }, - apiStatusRow: { + statusHeader: { flexDirection: "row", alignItems: "center", - gap: 8, + justifyContent: "space-between", + marginBottom: theme.spacing.md, }, - statusIndicatorDot: { - height: 12, - width: 12, + statusTitle: { + fontSize: theme.fontSize.lg, + fontWeight: "600", + color: theme.colors.cardForeground, + }, + statusBadge: { + backgroundColor: theme.colors.secondary, + paddingHorizontal: theme.spacing.sm + 4, + paddingVertical: theme.spacing.xs, borderRadius: 9999, }, - statusIndicatorGreen: { + statusBadgeText: { + fontSize: theme.fontSize.xs, + fontWeight: "500", + color: theme.colors.secondaryForeground, + }, + statusRow: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing.sm + 4, + }, + statusDot: { + height: 12, + width: 12, + borderRadius: 6, + }, + statusDotSuccess: { backgroundColor: theme.colors.success, }, - statusIndicatorRed: { - backgroundColor: theme.colors.destructive, + statusDotWarning: { + backgroundColor: "#F59E0B", }, - statusText: { - color: theme?.colors?.typography, + statusContent: { + flex: 1, }, + statusLabel: { + fontSize: theme.fontSize.sm, + fontWeight: "500", + color: theme.colors.cardForeground, + }, + statusDescription: { + fontSize: theme.fontSize.xs, + color: theme.colors.mutedForeground, + } })); diff --git a/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx b/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx index 9d37843..ebe089e 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx @@ -1,34 +1,65 @@ -import { Link, Stack } from "expo-router"; -import { Text } from "react-native"; -import { StyleSheet } from "react-native-unistyles"; - import { Container } from "@/components/container"; +import { Link, Stack } from "expo-router"; +import { Text, View } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; export default function NotFoundScreen() { return ( <> - This screen doesn't exist. - - Go to home screen! - + + + 🤔 + Page Not Found + + Sorry, the page you're looking for doesn't exist. + + + Go to Home + + + ); } const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: theme.spacing.lg, + }, + content: { + alignItems: "center", + }, + emoji: { + fontSize: 64, + marginBottom: theme.spacing.md, + }, title: { - fontSize: 20, + fontSize: theme.fontSize["2xl"], fontWeight: "bold", - color: theme.colors.typography, + color: theme.colors.foreground, + marginBottom: theme.spacing.sm, + textAlign: "center", }, - link: { - marginTop: 16, - paddingVertical: 16, + description: { + color: theme.colors.mutedForeground, + textAlign: "center", + marginBottom: theme.spacing.xl, + maxWidth: 280, }, - linkText: { - fontSize: 14, + button: { + backgroundColor: `${theme.colors.primary}1A`, // 10% opacity + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.sm + 4, + borderRadius: theme.borderRadius.lg, + }, + buttonText: { + color: theme.colors.primary, + fontWeight: "500", }, })); diff --git a/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs index 11cd4f3..d625a5a 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs +++ b/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs @@ -1,3 +1,6 @@ +{{#if (includes examples "ai")}} +import "@/polyfills"; +{{/if}} {{#if (eq api "trpc")}} import { queryClient } from "@/utils/trpc"; {{/if}} @@ -39,9 +42,9 @@ export default function RootLayout() { backgroundColor: theme.colors.background, }, headerTitleStyle: { - color: theme.colors.typography, + color: theme.colors.foreground, }, - headerTintColor: theme.colors.typography, + headerTintColor: theme.colors.foreground, }} > @@ -62,9 +65,9 @@ export default function RootLayout() { backgroundColor: theme.colors.background, }, headerTitleStyle: { - color: theme.colors.typography, + color: theme.colors.foreground, }, - headerTintColor: theme.colors.typography, + headerTintColor: theme.colors.foreground, }} > @@ -83,9 +86,9 @@ export default function RootLayout() { backgroundColor: theme.colors.background, }, headerTitleStyle: { - color: theme.colors.typography, + color: theme.colors.foreground, }, - headerTintColor: theme.colors.typography, + headerTintColor: theme.colors.foreground, }} > diff --git a/apps/cli/templates/frontend/native/unistyles/app/modal.tsx b/apps/cli/templates/frontend/native/unistyles/app/modal.tsx index 1259346..18941b6 100644 --- a/apps/cli/templates/frontend/native/unistyles/app/modal.tsx +++ b/apps/cli/templates/frontend/native/unistyles/app/modal.tsx @@ -1,29 +1,33 @@ import { Container } from "@/components/container"; -import { StatusBar } from "expo-status-bar"; -import { Platform, Text, View } from "react-native"; +import { Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; export default function Modal() { return ( - <> - - - - Model + + + + Modal - - + + ); } const styles = StyleSheet.create((theme) => ({ - text: { - color: theme.colors.typography, - }, container: { flex: 1, - paddingBottom: 100, - justifyContent: "center", + padding: theme.spacing.lg, + }, + header: { + flexDirection: "row", alignItems: "center", + justifyContent: "space-between", + marginBottom: theme.spacing.xl, + }, + title: { + fontSize: theme.fontSize["2xl"], + fontWeight: "bold", + color: theme.colors.foreground, }, })); diff --git a/apps/cli/templates/frontend/native/unistyles/components/container.tsx b/apps/cli/templates/frontend/native/unistyles/components/container.tsx index 44a18b3..06d670d 100644 --- a/apps/cli/templates/frontend/native/unistyles/components/container.tsx +++ b/apps/cli/templates/frontend/native/unistyles/components/container.tsx @@ -1,20 +1,15 @@ import React from "react"; -import { View } from "react-native"; +import { SafeAreaView } from "react-native"; import { StyleSheet } from "react-native-unistyles"; export const Container = ({ children }: { children: React.ReactNode }) => { - return {children}; + return {children}; }; const styles = StyleSheet.create((theme, rt) => ({ container: { flex: 1, - paddingBottom: rt.insets.bottom, backgroundColor: theme.colors.background, - transform: [ - { - translateY: rt.insets.ime * -1, - }, - ], + paddingBottom: rt.insets.bottom, }, })); diff --git a/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx b/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx index c998f53..d6e05d8 100644 --- a/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx +++ b/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx @@ -1,31 +1,36 @@ -import FontAwesome from '@expo/vector-icons/FontAwesome'; -import { forwardRef } from 'react'; -import { Pressable, StyleSheet } from 'react-native'; +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import { forwardRef } from "react"; +import { Pressable } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; -export const HeaderButton = forwardRef void }>( - ({ onPress }, ref) => { - return ( - - {({ pressed }) => ( - - )} - - ); - } -); - -export const styles = StyleSheet.create({ - headerRight: { - marginRight: 15, - }, +export const HeaderButton = forwardRef< + typeof Pressable, + { onPress?: () => void } +>(({ onPress }, ref) => { + return ( + + {({ pressed }) => ( + + )} + + ); }); + +const styles = StyleSheet.create((theme) => ({ + button: { + padding: theme.spacing.sm, + marginRight: theme.spacing.sm, + borderRadius: theme.borderRadius.lg, + backgroundColor: `${theme.colors.secondary}80`, // 50% opacity + }, + icon: { + color: theme.colors.secondaryForeground, + }, +})); diff --git a/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx b/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx index e75c9d3..3ea0888 100644 --- a/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx +++ b/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx @@ -1,15 +1,8 @@ -import FontAwesome from '@expo/vector-icons/FontAwesome'; -import { StyleSheet } from 'react-native'; +import FontAwesome from "@expo/vector-icons/FontAwesome"; export const TabBarIcon = (props: { - name: React.ComponentProps['name']; + name: React.ComponentProps["name"]; color: string; }) => { - return ; + return ; }; - -export const styles = StyleSheet.create({ - tabBarIcon: { - marginBottom: -3, - }, -}); diff --git a/apps/cli/templates/frontend/native/unistyles/package.json b/apps/cli/templates/frontend/native/unistyles/package.json.hbs similarity index 88% rename from apps/cli/templates/frontend/native/unistyles/package.json rename to apps/cli/templates/frontend/native/unistyles/package.json.hbs index 988b8cc..2049480 100644 --- a/apps/cli/templates/frontend/native/unistyles/package.json +++ b/apps/cli/templates/frontend/native/unistyles/package.json.hbs @@ -14,6 +14,10 @@ "@react-navigation/bottom-tabs": "^7.0.5", "@react-navigation/drawer": "^7.0.0", "@react-navigation/native": "^7.0.3", + {{#if (includes examples "ai")}} + "@stardazed/streams-text-encoding": "^1.0.2", + "@ungap/structured-clone": "^1.3.0", + {{/if}} "@tanstack/react-form": "^1.0.5", "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417", "expo": "^53.0.8", @@ -34,7 +38,7 @@ "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.10.0", - "react-native-unistyles": "3.0.0-rc.3", + "react-native-unistyles": "^3.0.0-rc.4", "react-native-web": "^0.20.0" }, "devDependencies": { diff --git a/apps/cli/templates/frontend/native/unistyles/theme.ts b/apps/cli/templates/frontend/native/unistyles/theme.ts index 12694bf..c754ee7 100644 --- a/apps/cli/templates/frontend/native/unistyles/theme.ts +++ b/apps/cli/templates/frontend/native/unistyles/theme.ts @@ -1,35 +1,98 @@ const sharedColors = { success: "#22C55E", - destructive: "#DC2626", - border: "#D1D5DB", + destructive: "#EF4444", + warning: "#F59E0B", + info: "#3B82F6", } as const; export const lightTheme = { colors: { ...sharedColors, - typography: "#000000", - background: "#ffffff", - primary: "#3B82F6", + typography: "hsl(222.2 84% 4.9%)", + background: "hsl(0 0% 100%)", + foreground: "hsl(222.2 84% 4.9%)", + card: "hsl(0 0% 100%)", + cardForeground: "hsl(222.2 84% 4.9%)", + primary: "hsl(221.2 83.2% 53.3%)", + primaryForeground: "hsl(210 40% 98%)", + secondary: "hsl(210 40% 96%)", + secondaryForeground: "hsl(222.2 84% 4.9%)", + muted: "hsl(210 40% 96%)", + mutedForeground: "hsl(215.4 16.3% 46.9%)", + accent: "hsl(210 40% 96%)", + accentForeground: "hsl(222.2 84% 4.9%)", + border: "hsl(214.3 31.8% 91.4%)", + input: "hsl(214.3 31.8% 91.4%)", + ring: "hsl(221.2 83.2% 53.3%)", }, - margins: { - sm: 2, - md: 4, - lg: 8, - xl: 12, + spacing: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, + }, + borderRadius: { + sm: 6, + md: 8, + lg: 12, + xl: 16, + }, + fontSize: { + xs: 12, + sm: 14, + base: 16, + lg: 18, + xl: 20, + "2xl": 24, + "3xl": 30, + "4xl": 36, }, } as const; export const darkTheme = { colors: { ...sharedColors, - typography: "#ffffff", - background: "#000000", - primary: "#60A5FA", + typography: "hsl(210 40% 98%)", + background: "hsl(222.2 84% 4.9%)", + foreground: "hsl(210 40% 98%)", + card: "hsl(222.2 84% 4.9%)", + cardForeground: "hsl(210 40% 98%)", + primary: "hsl(217.2 91.2% 59.8%)", + primaryForeground: "hsl(222.2 84% 4.9%)", + secondary: "hsl(217.2 32.6% 17.5%)", + secondaryForeground: "hsl(210 40% 98%)", + muted: "hsl(217.2 32.6% 17.5%)", + mutedForeground: "hsl(215 20.2% 65.1%)", + accent: "hsl(217.2 32.6% 17.5%)", + accentForeground: "hsl(210 40% 98%)", + border: "hsl(217.2 32.6% 17.5%)", + input: "hsl(217.2 32.6% 17.5%)", + ring: "hsl(224.3 76.3% 94.1%)", }, - margins: { - sm: 2, - md: 4, - lg: 8, - xl: 12, + spacing: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, + }, + borderRadius: { + sm: 6, + md: 8, + lg: 12, + xl: 16, + }, + fontSize: { + xs: 12, + sm: 14, + base: 16, + lg: 18, + xl: 20, + "2xl": 24, + "3xl": 30, + "4xl": 36, }, } as const; diff --git a/apps/cli/templates/frontend/native/unistyles/unistyles.ts b/apps/cli/templates/frontend/native/unistyles/unistyles.ts index 888a700..a645b21 100644 --- a/apps/cli/templates/frontend/native/unistyles/unistyles.ts +++ b/apps/cli/templates/frontend/native/unistyles/unistyles.ts @@ -1,7 +1,7 @@ -import { StyleSheet } from 'react-native-unistyles'; +import { StyleSheet } from "react-native-unistyles"; -import { breakpoints } from './breakpoints'; -import { lightTheme, darkTheme } from './theme'; +import { breakpoints } from "./breakpoints"; +import { darkTheme, lightTheme } from "./theme"; type AppBreakpoints = typeof breakpoints; @@ -10,7 +10,7 @@ type AppThemes = { dark: typeof darkTheme; }; -declare module 'react-native-unistyles' { +declare module "react-native-unistyles" { export interface UnistylesBreakpoints extends AppBreakpoints {} export interface UnistylesThemes extends AppThemes {} } diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index d281bb3..66c4b1e 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -626,90 +626,35 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } const incompatibleExamples: string[] = []; - const isWeb = hasWebFrontend(nextStack.webFrontend); - const hasNative = checkHasNativeFrontend(nextStack.nativeFrontend); - const isNativeOnly = hasNative && !isWeb; - if (isNativeOnly) { - if (nextStack.examples.length > 0) { - notes.webFrontend.notes.push( - "Examples are not supported with Native-only frontend. Examples will be removed.", - ); - notes.examples.notes.push( - "Examples require a web frontend. They will be removed.", - ); - notes.webFrontend.hasIssue = true; - notes.examples.hasIssue = true; - incompatibleExamples.push(...nextStack.examples); - changes.push({ - category: "examples", - message: - "Examples removed (not supported with Native-only frontend)", - }); - } - } else { - if (!isWeb) { - if (nextStack.examples.includes("todo")) { - incompatibleExamples.push("todo"); - changes.push({ - category: "examples", - message: "Todo example removed (requires web frontend)", - }); - } - if (nextStack.examples.includes("ai")) { - incompatibleExamples.push("ai"); - changes.push({ - category: "examples", - message: "AI example removed (requires web frontend)", - }); - } - } - if ( - nextStack.database === "none" && - nextStack.examples.includes("todo") - ) { - incompatibleExamples.push("todo"); - changes.push({ - category: "examples", - message: "Todo example removed (requires a database)", - }); - } - if ( - nextStack.backend === "elysia" && - nextStack.examples.includes("ai") - ) { - incompatibleExamples.push("ai"); - changes.push({ - category: "examples", - message: "AI example removed (not compatible with Elysia)", - }); - } - if (isSolid && nextStack.examples.includes("ai")) { - incompatibleExamples.push("ai"); - changes.push({ - category: "examples", - message: "AI example removed (not compatible with Solid)", - }); - } + // Note: Examples are now supported with Native-only frontends + if ( + nextStack.database === "none" && + nextStack.examples.includes("todo") + ) { + incompatibleExamples.push("todo"); + changes.push({ + category: "examples", + message: "Todo example removed (requires a database)", + }); + } + if (nextStack.backend === "elysia" && nextStack.examples.includes("ai")) { + incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (not compatible with Elysia)", + }); + } + if (isSolid && nextStack.examples.includes("ai")) { + incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (not compatible with Solid)", + }); } const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; if (uniqueIncompatibleExamples.length > 0) { - if (!isWeb && !isNativeOnly) { - if ( - uniqueIncompatibleExamples.includes("todo") || - uniqueIncompatibleExamples.includes("ai") - ) { - notes.webFrontend.notes.push( - "Examples require a web frontend. Incompatible examples will be removed.", - ); - notes.examples.notes.push( - "Requires a web frontend. Incompatible examples will be removed.", - ); - notes.webFrontend.hasIssue = true; - notes.examples.hasIssue = true; - } - } if ( nextStack.database === "none" && uniqueIncompatibleExamples.includes("todo") @@ -769,7 +714,6 @@ const getCompatibilityRules = (stack: StackState) => { const isBackendNone = stack.backend === "none"; const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend); const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend); - const hasNativeOnly = hasNativeFrontend && !hasWebFrontendSelected; const hasSolid = stack.webFrontend.includes("solid"); const hasNuxt = stack.webFrontend.includes("nuxt"); const hasSvelte = stack.webFrontend.includes("svelte"); @@ -779,7 +723,6 @@ const getCompatibilityRules = (stack: StackState) => { isBackendNone, hasWebFrontend: hasWebFrontendSelected, hasNativeFrontend, - hasNativeOnly, hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend), hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend), hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid, @@ -1328,59 +1271,37 @@ const StackBuilder = () => { } if (catKey === "examples" && techId !== "none") { - if (rules.hasNativeOnly) { + if (stack.api === "none" && !rules.isConvex && !rules.isBackendNone) { addRule( category, techId, - "Disabled: Examples are not supported with a Native-only frontend.", + "Disabled: Examples require an API. Cannot be selected when API is 'None'.", + ); + } + if ( + stack.database === "none" && + techId === "todo" && + !rules.isConvex + ) { + addRule( + category, + techId, + "Disabled: The 'Todo' example requires a database.", + ); + } + if (stack.backend === "elysia" && techId === "ai") { + addRule( + category, + techId, + "Disabled: The 'AI' example is not compatible with an Elysia backend.", + ); + } + if (rules.hasSolid && techId === "ai") { + addRule( + category, + techId, + "Disabled: The 'AI' example is not compatible with a Solid frontend.", ); - } else { - if ( - !rules.hasWebFrontend && - (techId === "todo" || techId === "ai") - ) { - addRule( - category, - techId, - "Disabled: This example requires a web frontend.", - ); - } - if ( - stack.api === "none" && - !rules.isConvex && - !rules.isBackendNone - ) { - addRule( - category, - techId, - "Disabled: Examples require an API. Cannot be selected when API is 'None'.", - ); - } - if ( - stack.database === "none" && - techId === "todo" && - !rules.isConvex - ) { - addRule( - category, - techId, - "Disabled: The 'Todo' example requires a database.", - ); - } - if (stack.backend === "elysia" && techId === "ai") { - addRule( - category, - techId, - "Disabled: The 'AI' example is not compatible with an Elysia backend.", - ); - } - if (rules.hasSolid && techId === "ai") { - addRule( - category, - techId, - "Disabled: The 'AI' example is not compatible with a Solid frontend.", - ); - } } } }