diff --git a/README.md b/README.md index 68c6c25b..c75a5584 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pnpm create better-t-stack@latest - **Zero-config setup** with interactive CLI wizard - **End-to-end type safety** from database to frontend via tRPC -- **Modern stack** with React, Hono/Elysia, and TanStack libraries +- **Modern stack** with React, Vue, Nuxt, Svelte, Solid, and more - **Multi-platform** supporting web, mobile (Expo), and desktop applications - **Database flexibility** with SQLite (Turso) or PostgreSQL options - **ORM choice** between Drizzle or Prisma diff --git a/apps/cli/README.md b/apps/cli/README.md index 94e541e4..173de20d 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -32,7 +32,7 @@ Follow the prompts to configure your project or use the `--yes` flag for default | Category | Options | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **TypeScript** | End-to-end type safety across all parts of your application | -| **Frontend** | • React with TanStack Router
• React with React Router
• React with TanStack Start (SSR)
• Next.js
• SvelteKit
• Nuxt (Vue)
• SolidJS
• React Native with NativeWind (via Expo)
• React Native with Unistyles (via Expo)
• None | +| **Frontend** | • React with TanStack Router
• React with React Router
• React with TanStack Start (SSR)
• Next.js
• Vue with Vue Router
• Nuxt (Vue)
• SvelteKit
• SolidJS
• React Native with NativeWind (via Expo)
• React Native with Unistyles (via Expo)
• None | | **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex
• Fastify
• None | | **API Layer** | • tRPC (type-safe APIs)
• oRPC (OpenAPI-compatible type-safe APIs)
• None | | **Runtime** | • Bun
• Node.js
• Cloudflare Workers
• None | @@ -57,7 +57,7 @@ Options: --orm ORM type (none, drizzle, prisma, mongoose) --auth Include authentication --no-auth Exclude authentication - --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none) + --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, vue-router, svelte, solid, native-nativewind, native-unistyles, none) --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none) --examples Examples to include (todo, ai, none) --git Initialize git repository diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index cdd55111..632efa5b 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -128,8 +128,8 @@ export const dependencyVersionMap = { export type AvailableDependencies = keyof typeof dependencyVersionMap; export const ADDON_COMPATIBILITY: Record = { - pwa: ["tanstack-router", "react-router", "solid", "next"], - tauri: ["tanstack-router", "react-router", "nuxt", "svelte", "solid"], + pwa: ["tanstack-router", "react-router", "solid", "next", "vue-router"], + tauri: ["tanstack-router", "react-router", "nuxt", "vue-router", "svelte", "solid"], biome: [], husky: [], turborepo: [], @@ -147,6 +147,7 @@ export const WEB_FRAMEWORKS: readonly Frontend[] = [ "tanstack-start", "next", "nuxt", + "vue-router", "svelte", "solid", ]; diff --git a/apps/cli/src/helpers/project-generation/template-manager.ts b/apps/cli/src/helpers/project-generation/template-manager.ts index 72197e99..ffe50369 100644 --- a/apps/cli/src/helpers/project-generation/template-manager.ts +++ b/apps/cli/src/helpers/project-generation/template-manager.ts @@ -67,6 +67,7 @@ export async function setupFrontendTemplates( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasVueRouterWeb = context.frontend.includes("vue-router"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); const hasNativeWind = context.frontend.includes("native-nativewind"); @@ -74,7 +75,7 @@ export async function setupFrontendTemplates( const _hasNative = hasNativeWind || hasUnistyles; const isConvex = context.backend === "convex"; - if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) { + if (hasReactWeb || hasNuxtWeb || hasVueRouterWeb || hasSvelteWeb || hasSolidWeb) { const webAppDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webAppDir); @@ -140,6 +141,23 @@ export async function setupFrontendTemplates( } else { } } + } else if (hasVueRouterWeb) { + const vueRouterBaseDir = path.join(PKG_ROOT, "templates/frontend/vue/vue-router"); + if (await fs.pathExists(vueRouterBaseDir)) { + await processAndCopyFiles("**/*", vueRouterBaseDir, webAppDir, context); + } else { + } + + if (!isConvex && context.api !== "none") { + const apiWebVueDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/vue`, + ); + if (await fs.pathExists(apiWebVueDir)) { + await processAndCopyFiles("**/*", apiWebVueDir, webAppDir, context); + } else { + } + } } else if (hasSvelteWeb) { const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte"); if (await fs.pathExists(svelteBaseDir)) { @@ -372,6 +390,7 @@ export async function setupAuthTemplate( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasVueRouterWeb = context.frontend.includes("vue-router"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); const hasNativeWind = context.frontend.includes("native-nativewind"); @@ -434,7 +453,7 @@ export async function setupAuthTemplate( } if ( - (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && + (hasReactWeb || hasNuxtWeb || hasVueRouterWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists ) { if (hasReactWeb) { @@ -473,6 +492,14 @@ export async function setupAuthTemplate( await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context); } else { } + } else if (hasVueRouterWeb) { + if (context.api !== "none") { + const authWebVueSrc = path.join(PKG_ROOT, "templates/auth/web/vue"); + if (await fs.pathExists(authWebVueSrc)) { + await processAndCopyFiles("**/*", authWebVueSrc, webAppDir, context); + } else { + } + } } else if (hasSvelteWeb) { const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte"); if (await fs.pathExists(authWebSvelteSrc)) { @@ -588,6 +615,7 @@ export async function setupExamplesTemplate( ["tanstack-router", "react-router", "tanstack-start", "next"].includes(f), ); const hasNuxtWeb = context.frontend.includes("nuxt"); + const hasVueRouterWeb = context.frontend.includes("vue-router"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); @@ -716,6 +744,18 @@ export async function setupExamplesTemplate( ); } else { } + } else if (hasVueRouterWeb) { + const exampleWebVueSrc = path.join(exampleBaseDir, "web/vue"); + if (await fs.pathExists(exampleWebVueSrc)) { + await processAndCopyFiles( + "**/*", + exampleWebVueSrc, + webAppDir, + context, + false, + ); + } else { + } } else if (hasSvelteWeb) { const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte"); if (await fs.pathExists(exampleWebSvelteSrc)) { @@ -863,6 +903,7 @@ export async function setupDeploymentTemplates( solid: "solid", next: "react/next", nuxt: "nuxt", + "vue-router": "vue", svelte: "svelte", }; diff --git a/apps/cli/src/prompts/frontend.ts b/apps/cli/src/prompts/frontend.ts index e4eb9c08..6bccb632 100644 --- a/apps/cli/src/prompts/frontend.ts +++ b/apps/cli/src/prompts/frontend.ts @@ -56,6 +56,11 @@ export async function getFrontendChoice( label: "Nuxt", hint: "The Progressive Web Framework for Vue.js", }, + { + value: "vue-router" as const, + label: "Vue Router", + hint: "The official router for Vue.js single-page applications", + }, { value: "svelte" as const, label: "Svelte", @@ -75,7 +80,7 @@ export async function getFrontendChoice( const webOptions = allWebOptions.filter((option) => { if (backend === "convex") { - return option.value !== "solid"; + return option.value !== "nuxt" && option.value !== "solid" && option.value !== "vue-router"; } return true; }); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 536a1748..1ad65b3d 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -29,6 +29,7 @@ export const FrontendSchema = z "tanstack-start", "next", "nuxt", + "vue-router", "native-nativewind", "native-unistyles", "svelte", diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index 10db63bf..53c2936f 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -135,7 +135,7 @@ export function processAndValidateFlags( if (webFrontends.length > 1) { consola.fatal( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, vue-router, svelte, solid", ); process.exit(1); } @@ -205,7 +205,7 @@ export function processAndValidateFlags( if (providedFlags.has("frontend") && options.frontend) { const incompatibleFrontends = options.frontend.filter( - (f) => f === "solid", + (f) => f === "nuxt" || f === "solid" || f === "vue-router", ); if (incompatibleFrontends.length > 0) { consola.fatal( @@ -632,9 +632,10 @@ export function validateConfigCompatibility(config: Partial) { process.exit(1); } - if (config.examples.includes("ai") && includesSolid) { + const includesVueRouter = effectiveFrontend?.includes("vue-router"); + if (config.examples.includes("ai") && (includesSolid || includesVueRouter)) { consola.fatal( - "The 'ai' example is not compatible with the Solid frontend.", + `The 'ai' example is not compatible with the ${includesSolid ? "Solid" : "Vue Router"} frontend.`, ); process.exit(1); } diff --git a/apps/cli/templates/api/orpc/web/vue/src/lib/api.ts.hbs b/apps/cli/templates/api/orpc/web/vue/src/lib/api.ts.hbs new file mode 100644 index 00000000..fe740aaa --- /dev/null +++ b/apps/cli/templates/api/orpc/web/vue/src/lib/api.ts.hbs @@ -0,0 +1,25 @@ +import { createORPCClient, createORPCFetchHandler } from "@orpc/client"; +import { ref } from "vue"; +import type { ORPCContext, orpc } from "../../../../server/src/lib/orpc"; + +export const getAuthToken = () => { + if (typeof window === "undefined") return undefined; + return localStorage.getItem("auth-token") ?? undefined; +}; + +export const authToken = ref(getAuthToken()); + +const fetchHandler = createORPCFetchHandler({ + baseURL: import.meta.env.VITE_SERVER_URL ?? "http://localhost:5000/api", + headers: () => { + const token = authToken.value; + return token ? { Authorization: `Bearer ${token}` } : undefined; + }, +}); + +export const { api } = createORPCClient< + typeof orpc, + ORPCContext +>({ + async: fetchHandler, +}); \ No newline at end of file diff --git a/apps/cli/templates/api/trpc/web/vue/src/lib/api.ts.hbs b/apps/cli/templates/api/trpc/web/vue/src/lib/api.ts.hbs new file mode 100644 index 00000000..9f74cce5 --- /dev/null +++ b/apps/cli/templates/api/trpc/web/vue/src/lib/api.ts.hbs @@ -0,0 +1,20 @@ +import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; +import superjson from "superjson"; +import type { AppRouter } from "../../../../server/src/router/root"; + +export const api = createTRPCProxyClient({ + links: [ + httpBatchLink({ + transformer: superjson, + url: import.meta.env.VITE_SERVER_URL ?? "http://localhost:5000/api/trpc", + async headers() { + const headers: Record = {}; + const token = localStorage.getItem("auth-token"); + if (token) { + headers.authorization = `Bearer ${token}`; + } + return headers; + }, + }), + ], +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/web/vue/src/lib/auth.ts.hbs b/apps/cli/templates/auth/web/vue/src/lib/auth.ts.hbs new file mode 100644 index 00000000..72843a83 --- /dev/null +++ b/apps/cli/templates/auth/web/vue/src/lib/auth.ts.hbs @@ -0,0 +1,89 @@ +import { ref, computed } from 'vue' +import { useRouter } from 'vue-router' +import { api{{#if (eq api "trpc")}}, authToken{{/if}} } from './api' + +export interface User { + id: string + email: string + name: string | null +} + +export interface Session { + user: User + expiresAt: Date +} + +const currentUser = ref(null) +const isLoading = ref(true) + +export function useAuth() { + const router = useRouter() + + const isAuthenticated = computed(() => !!currentUser.value) + + const initialize = async () => { + try { + const token = localStorage.getItem('auth-token') + if (!token) { + isLoading.value = false + return + } + + const { data } = await api.auth.getSession{{#if (eq api "orpc")}}(){{/if}} + if (data) { + currentUser.value = data.user +{{#if (eq api "orpc")}} authToken.value = token{{/if}} + } + } catch (error) { + localStorage.removeItem('auth-token') +{{#if (eq api "orpc")}} authToken.value = null{{/if}} + } finally { + isLoading.value = false + } + } + + const login = async (email: string, password: string) => { + const { data } = await api.auth.login{{#if (eq api "orpc")}}({email, password}){{else}}({ email, password }){{/if}} + + if (data?.token) { + localStorage.setItem('auth-token', data.token) +{{#if (eq api "orpc")}} authToken.value = data.token{{/if}} + currentUser.value = data.user + router.push('/') + } + } + + const signup = async (email: string, password: string, name: string) => { + const { data } = await api.auth.signup{{#if (eq api "orpc")}}({email, password, name}){{else}}({ email, password, name }){{/if}} + + if (data?.token) { + localStorage.setItem('auth-token', data.token) +{{#if (eq api "orpc")}} authToken.value = data.token{{/if}} + currentUser.value = data.user + router.push('/') + } + } + + const logout = async () => { + try { + await api.auth.logout{{#if (eq api "orpc")}}(){{/if}} + } catch (error) { + console.error('Logout error:', error) + } finally { + localStorage.removeItem('auth-token') +{{#if (eq api "orpc")}} authToken.value = null{{/if}} + currentUser.value = null + router.push('/login') + } + } + + return { + user: currentUser, + isAuthenticated, + isLoading, + initialize, + login, + signup, + logout, + } +} \ No newline at end of file diff --git a/apps/cli/templates/deploy/web/vue/_worker.js b/apps/cli/templates/deploy/web/vue/_worker.js new file mode 100644 index 00000000..27a51071 --- /dev/null +++ b/apps/cli/templates/deploy/web/vue/_worker.js @@ -0,0 +1,24 @@ +export default { + async fetch(request, env) { + const url = new URL(request.url); + + // Serve static assets + try { + // Check if the path has a file extension + const hasExtension = url.pathname.split('/').pop().includes('.'); + + if (hasExtension) { + // Try to serve the static asset + return await env.ASSETS.fetch(request); + } + + // For paths without extensions, serve index.html (SPA routing) + const indexRequest = new Request(new URL('/index.html', request.url).toString(), request); + return await env.ASSETS.fetch(indexRequest); + } catch (e) { + // If the asset doesn't exist, serve index.html + const indexRequest = new Request(new URL('/index.html', request.url).toString(), request); + return await env.ASSETS.fetch(indexRequest); + } + }, +}; \ No newline at end of file diff --git a/apps/cli/templates/deploy/web/vue/package.json.hbs b/apps/cli/templates/deploy/web/vue/package.json.hbs new file mode 100644 index 00000000..9cf6a500 --- /dev/null +++ b/apps/cli/templates/deploy/web/vue/package.json.hbs @@ -0,0 +1,8 @@ +{ + "scripts": { + "deploy": "wrangler pages deploy dist" + }, + "devDependencies": { + "wrangler": "^3.101.0" + } +} \ No newline at end of file diff --git a/apps/cli/templates/deploy/web/vue/wrangler.toml.hbs b/apps/cli/templates/deploy/web/vue/wrangler.toml.hbs new file mode 100644 index 00000000..e9346b4a --- /dev/null +++ b/apps/cli/templates/deploy/web/vue/wrangler.toml.hbs @@ -0,0 +1,9 @@ +name = "{{projectName}}-web" +compatibility_date = "2024-12-01" +compatibility_flags = ["nodejs_compat"] +main = "./dist/_worker.js" + +[site] +bucket = "./dist" + +[env.production] \ No newline at end of file diff --git a/apps/cli/templates/examples/todo/web/vue/src/views/Todo.vue.hbs b/apps/cli/templates/examples/todo/web/vue/src/views/Todo.vue.hbs new file mode 100644 index 00000000..057ee3e6 --- /dev/null +++ b/apps/cli/templates/examples/todo/web/vue/src/views/Todo.vue.hbs @@ -0,0 +1,112 @@ + + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/env.d.ts b/apps/cli/templates/frontend/vue/vue-router/env.d.ts new file mode 100644 index 00000000..151aa685 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/index.html.hbs b/apps/cli/templates/frontend/vue/vue-router/index.html.hbs new file mode 100644 index 00000000..3bc90f61 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/index.html.hbs @@ -0,0 +1,13 @@ + + + + + + + {{projectName}} - Vue Router App + + +
+ + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/package.json.hbs b/apps/cli/templates/frontend/vue/vue-router/package.json.hbs new file mode 100644 index 00000000..c38022ec --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/package.json.hbs @@ -0,0 +1,43 @@ +{ + "name": "web", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "typecheck": "vue-tsc --build --force" + }, + "dependencies": { + "@tanstack/vue-form": "^1.12.0", + "@tanstack/vue-query": "^5.67.2", + "@tanstack/vue-virtual": "^3.12.0", + "@vueuse/core": "^12.3.1", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-vue-next": "^0.511.0", + "pinia": "^2.3.0", + "radix-vue": "^1.9.13", + "tailwind-merge": "^3.3.0", + "tw-animate-css": "^1.3.2", + "vee-validate": "^4.14.10", + "vue": "^3.6.3", + "vue-router": "^4.5.0", + "vue-sonner": "^2.0.0", + "zod": "^4.0.2"{{#if (eq api "trpc")}}, + "@trpc/client": "catalog:trpc11", + "superjson": "^2.2.1"{{/if}}{{#if (eq api "orpc")}}, + "@orpc/client": "catalog:orpc"{{/if}} + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.8", + "@types/node": "^20", + "@vitejs/plugin-vue": "^5.2.1", + "tailwindcss": "^4.1.8", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4", + "vue-tsc": "^2.2.0" + } +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/public/favicon.ico b/apps/cli/templates/frontend/vue/vue-router/public/favicon.ico new file mode 100644 index 00000000..5dbdfcdd Binary files /dev/null and b/apps/cli/templates/frontend/vue/vue-router/public/favicon.ico differ diff --git a/apps/cli/templates/frontend/vue/vue-router/src/App.vue.hbs b/apps/cli/templates/frontend/vue/vue-router/src/App.vue.hbs new file mode 100644 index 00000000..d75d7910 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/App.vue.hbs @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/assets/main.css b/apps/cli/templates/frontend/vue/vue-router/src/assets/main.css new file mode 100644 index 00000000..2f9dd454 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/assets/main.css @@ -0,0 +1,70 @@ +@import 'tailwindcss'; + +@theme { + --font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + + --color-background: #ffffff; + --color-foreground: #0a0a0a; + --color-card: #ffffff; + --color-card-foreground: #0a0a0a; + --color-popover: #ffffff; + --color-popover-foreground: #0a0a0a; + --color-primary: #171717; + --color-primary-foreground: #fafafa; + --color-secondary: #f5f5f5; + --color-secondary-foreground: #171717; + --color-muted: #f5f5f5; + --color-muted-foreground: #737373; + --color-accent: #f5f5f5; + --color-accent-foreground: #171717; + --color-destructive: #ef4444; + --color-destructive-foreground: #fafafa; + --color-border: #e5e5e5; + --color-input: #e5e5e5; + --color-ring: #0a0a0a; + --color-chart-1: #e76e50; + --color-chart-2: #2a9d90; + --color-chart-3: #274754; + --color-chart-4: #e8c468; + --color-chart-5: #f4a462; + + --radius-sm: 0.125rem; + --radius-md: 0.375rem; + --radius-lg: 0.5rem; + --radius-xl: 1rem; +} + +html { + font-family: var(--font-family-sans); +} + +.dark { + --color-background: #0a0a0a; + --color-foreground: #fafafa; + --color-card: #0a0a0a; + --color-card-foreground: #fafafa; + --color-popover: #0a0a0a; + --color-popover-foreground: #fafafa; + --color-primary: #fafafa; + --color-primary-foreground: #171717; + --color-secondary: #262626; + --color-secondary-foreground: #fafafa; + --color-muted: #262626; + --color-muted-foreground: #a3a3a3; + --color-accent: #262626; + --color-accent-foreground: #fafafa; + --color-destructive: #7f1d1d; + --color-destructive-foreground: #fafafa; + --color-border: #262626; + --color-input: #262626; + --color-ring: #fafafa; + --color-chart-1: #e76e50; + --color-chart-2: #2a9d90; + --color-chart-3: #264754; + --color-chart-4: #e8c468; + --color-chart-5: #f4a462; +} + +* { + border-color: var(--color-border); +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/mode-toggle.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/mode-toggle.tsx new file mode 100644 index 00000000..7ed29edb --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/mode-toggle.tsx @@ -0,0 +1,44 @@ +import { defineComponent, computed } from 'vue' +import { useColorMode } from '@vueuse/core' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Moon, Sun } from 'lucide-vue-next' + +export const ModeToggle = defineComponent({ + name: 'ModeToggle', + setup() { + const mode = useColorMode() + + const setTheme = (theme: string) => { + mode.value = theme + } + + return () => ( + + + + + + setTheme('light')}> + Light + + setTheme('dark')}> + Dark + + setTheme('auto')}> + System + + + + ) + } +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/ui/button.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/button.tsx new file mode 100644 index 00000000..7d482a4b --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/button.tsx @@ -0,0 +1,76 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' +import { defineComponent, type PropType } from 'vue' +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps + extends VariantProps { + asChild?: boolean + class?: string + type?: 'button' | 'submit' | 'reset' +} + +export const Button = defineComponent({ + name: 'Button', + props: { + variant: { + type: String as PropType, + default: 'default', + }, + size: { + type: String as PropType, + default: 'default', + }, + class: String, + type: { + type: String as PropType, + default: 'button', + }, + asChild: { + type: Boolean, + default: false, + }, + }, + setup(props, { slots }) { + return () => { + const Tag = props.asChild ? 'div' : 'button' + return ( + + {slots.default?.()} + + ) + } + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/ui/card.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/card.tsx new file mode 100644 index 00000000..587215a1 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import { defineComponent } from 'vue' +import { cn } from '@/lib/utils' + +export const Card = defineComponent({ + name: 'Card', + props: { + class: String, + }, + setup(props, { slots }) { + return () => ( +
+ {slots.default?.()} +
+ ) + }, +}) + +export const CardHeader = defineComponent({ + name: 'CardHeader', + props: { + class: String, + }, + setup(props, { slots }) { + return () => ( +
+ {slots.default?.()} +
+ ) + }, +}) + +export const CardTitle = defineComponent({ + name: 'CardTitle', + props: { + class: String, + }, + setup(props, { slots }) { + return () => ( +

+ {slots.default?.()} +

+ ) + }, +}) + +export const CardDescription = defineComponent({ + name: 'CardDescription', + props: { + class: String, + }, + setup(props, { slots }) { + return () => ( +

+ {slots.default?.()} +

+ ) + }, +}) + +export const CardContent = defineComponent({ + name: 'CardContent', + props: { + class: String, + }, + setup(props, { slots }) { + return () =>
{slots.default?.()}
+ }, +}) + +export const CardFooter = defineComponent({ + name: 'CardFooter', + props: { + class: String, + }, + setup(props, { slots }) { + return () => ( +
+ {slots.default?.()} +
+ ) + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/ui/checkbox.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/checkbox.tsx new file mode 100644 index 00000000..f17bb13f --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/checkbox.tsx @@ -0,0 +1,34 @@ +import { defineComponent, type PropType } from 'vue' +import { CheckboxIndicator, CheckboxRoot } from 'radix-vue' +import { Check } from 'lucide-vue-next' +import { cn } from '@/lib/utils' + +export const Checkbox = defineComponent({ + name: 'Checkbox', + props: { + checked: { + type: [Boolean, String] as PropType, + default: false, + }, + disabled: Boolean, + class: String, + }, + emits: ['update:checked'], + setup(props, { emit }) { + return () => ( + emit('update:checked', value)} + class={cn( + 'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', + props.class + )} + > + + + + + ) + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/ui/dropdown-menu.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..50daad87 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,81 @@ +import { defineComponent, type PropType } from 'vue' +import { + DropdownMenuContent as RadixDropdownMenuContent, + DropdownMenuItem as RadixDropdownMenuItem, + DropdownMenuPortal, + DropdownMenuRoot, + DropdownMenuTrigger as RadixDropdownMenuTrigger, + type DropdownMenuContentProps, +} from 'radix-vue' +import { cn } from '@/lib/utils' + +export const DropdownMenu = defineComponent({ + name: 'DropdownMenu', + setup(_, { slots }) { + return () => {slots.default?.()} + }, +}) + +export const DropdownMenuTrigger = defineComponent({ + name: 'DropdownMenuTrigger', + props: { + asChild: { + type: Boolean, + default: false, + }, + }, + setup(props, { slots }) { + return () => ( + + {slots.default?.()} + + ) + }, +}) + +export const DropdownMenuContent = defineComponent({ + name: 'DropdownMenuContent', + props: { + align: { + type: String as PropType, + default: 'center', + }, + class: String, + }, + setup(props, { slots }) { + return () => ( + + + {slots.default?.()} + + + ) + }, +}) + +export const DropdownMenuItem = defineComponent({ + name: 'DropdownMenuItem', + props: { + class: String, + onClick: Function as PropType<() => void>, + }, + setup(props, { slots }) { + return () => ( + + {slots.default?.()} + + ) + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/components/ui/input.tsx b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/input.tsx new file mode 100644 index 00000000..e41bcd23 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/components/ui/input.tsx @@ -0,0 +1,36 @@ +import { defineComponent, type InputHTMLAttributes } from 'vue' +import { cn } from '@/lib/utils' + +export interface InputProps extends InputHTMLAttributes { + class?: string +} + +export const Input = defineComponent({ + name: 'Input', + props: { + class: String, + type: { + type: String, + default: 'text', + }, + placeholder: String, + disabled: Boolean, + modelValue: [String, Number], + }, + emits: ['update:modelValue'], + setup(props, { emit }) { + return () => ( + emit('update:modelValue', (e.target as HTMLInputElement).value)} + /> + ) + }, +}) \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/lib/utils.ts b/apps/cli/templates/frontend/vue/vue-router/src/lib/utils.ts new file mode 100644 index 00000000..312ce6aa --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/main.ts.hbs b/apps/cli/templates/frontend/vue/vue-router/src/main.ts.hbs new file mode 100644 index 00000000..f5e5bff7 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/main.ts.hbs @@ -0,0 +1,14 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import { VueQueryPlugin } from '@tanstack/vue-query' +import App from './App.vue' +import router from './router' +import './assets/main.css' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(VueQueryPlugin) + +app.mount('#app') \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/router/index.ts.hbs b/apps/cli/templates/frontend/vue/vue-router/src/router/index.ts.hbs new file mode 100644 index 00000000..b408a3da --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/router/index.ts.hbs @@ -0,0 +1,23 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '../views/Home.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + }, + { + path: '/about', + name: 'about', + // route level code-splitting + // this generates a separate chunk (About.[hash].js) for this route + // which is lazy-loaded when the route is visited. + component: () => import('../views/About.vue') + } + ] +}) + +export default router \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/views/About.vue.hbs b/apps/cli/templates/frontend/vue/vue-router/src/views/About.vue.hbs new file mode 100644 index 00000000..c9b41d5d --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/views/About.vue.hbs @@ -0,0 +1,50 @@ + + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/src/views/Home.vue.hbs b/apps/cli/templates/frontend/vue/vue-router/src/views/Home.vue.hbs new file mode 100644 index 00000000..ed524c7c --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/src/views/Home.vue.hbs @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/tsconfig.app.json b/apps/cli/templates/frontend/vue/vue-router/tsconfig.app.json new file mode 100644 index 00000000..3c0501c0 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + }, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/tsconfig.json.hbs b/apps/cli/templates/frontend/vue/vue-router/tsconfig.json.hbs new file mode 100644 index 00000000..6ce264f5 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/tsconfig.json.hbs @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "paths": { + "@/*": ["./src/*"] + }, + "resolveJsonModule": true, + "jsx": "preserve", + "noEmit": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true + }, + "include": ["src", "env.d.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/tsconfig.node.json b/apps/cli/templates/frontend/vue/vue-router/tsconfig.node.json new file mode 100644 index 00000000..17c61baf --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/tsconfig.node.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + }, + "include": ["vite.config.ts"] +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/vue/vue-router/vite.config.ts.hbs b/apps/cli/templates/frontend/vue/vue-router/vite.config.ts.hbs new file mode 100644 index 00000000..d7a22970 --- /dev/null +++ b/apps/cli/templates/frontend/vue/vue-router/vite.config.ts.hbs @@ -0,0 +1,14 @@ +import vue from '@vitejs/plugin-vue' +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + tsconfigPaths(), + ], + server: { + port: 3000, + }, +}); \ No newline at end of file diff --git a/apps/web/public/schema.json b/apps/web/public/schema.json index 1e5780f4..3e2b3377 100644 --- a/apps/web/public/schema.json +++ b/apps/web/public/schema.json @@ -73,6 +73,7 @@ "tanstack-start", "next", "nuxt", + "vue-router", "native-nativewind", "native-unistyles", "svelte",