diff --git a/apps/v4/app/(create)/lib/v0.ts b/apps/v4/app/(create)/lib/v0.ts index a6960623ca..6e6eeb64e2 100644 --- a/apps/v4/app/(create)/lib/v0.ts +++ b/apps/v4/app/(create)/lib/v0.ts @@ -17,42 +17,7 @@ import { const { Index } = await import("@/registry/bases/__index__") -// Builds a full v0 payload from a design system config. -export async function buildV0Payload(designSystemConfig: DesignSystemConfig) { - const registryBase = buildRegistryBase(designSystemConfig) - - // Build all files in parallel. - const [globalsCss, layoutFile, componentFiles] = await Promise.all([ - buildGlobalsCss(registryBase), - buildLayoutFile(designSystemConfig), - buildComponentFiles(designSystemConfig), - ]) - - return registryItemSchema.parse({ - name: designSystemConfig.item ?? "Item", - type: "registry:item", - dependencies: registryBase.dependencies, - files: [globalsCss, layoutFile, ...componentFiles], - }) -} - -function buildGlobalsCss(registryBase: RegistryItem) { - const lightVars = Object.entries(registryBase.cssVars?.light ?? {}) - .map(([key, value]) => ` --${key}: ${value};`) - .join("\n") - - const darkVars = Object.entries(registryBase.cssVars?.dark ?? {}) - .map(([key, value]) => ` --${key}: ${value};`) - .join("\n") - - const content = dedent`@import "tailwindcss"; -@import "tw-animate-css"; -/* @import "shadcn/tailwind.css"; */ - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --font-sans: var(--font-sans); +const THEME_INLINE = `--font-sans: var(--font-sans); --font-mono: var(--font-mono); --font-serif: var(--font-serif); --color-background: var(--background); @@ -93,7 +58,129 @@ function buildGlobalsCss(registryBase: RegistryItem) { --radius-xl: calc(var(--radius) * 1.4); --radius-2xl: calc(var(--radius) * 1.8); --radius-3xl: calc(var(--radius) * 2.2); - --radius-4xl: calc(var(--radius) * 2.6); + --radius-4xl: calc(var(--radius) * 2.6);` + +// Static file — parsed once at module level. +const themeProviderFile = registryItemFileSchema.parse({ + path: "components/theme-provider.tsx", + type: "registry:component", + target: "components/theme-provider.tsx", + content: dedent` + "use client" + + import * as React from "react" + import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes" + + function ThemeProvider({ + children, + ...props + }: React.ComponentProps) { + return ( + + + {children} + + ) + } + + function isTypingTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + + return ( + target.isContentEditable || + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.tagName === "SELECT" + ) + } + + function ThemeHotkey() { + const { resolvedTheme, setTheme } = useTheme() + + React.useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.defaultPrevented || event.repeat) { + return + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return + } + + if (event.key.toLowerCase() !== "d") { + return + } + + if (isTypingTarget(event.target)) { + return + } + + setTheme(resolvedTheme === "dark" ? "light" : "dark") + } + + window.addEventListener("keydown", onKeyDown) + + return () => { + window.removeEventListener("keydown", onKeyDown) + } + }, [resolvedTheme, setTheme]) + + return null + } + + export { ThemeProvider } + `, +}) + +const transformers = [transformIcons, transformMenu, transformRender] + +// Reuse a single ts-morph Project — avoids re-creating the compiler host per file. +const project = new Project({ compilerOptions: {} }) + +// Builds a full v0 payload from a design system config. +export async function buildV0Payload(designSystemConfig: DesignSystemConfig) { + const registryBase = buildRegistryBase(designSystemConfig) + + // Build all files in parallel. + const [globalsCss, layoutFile, componentFiles] = await Promise.all([ + buildGlobalsCss(registryBase), + buildLayoutFile(designSystemConfig), + buildComponentFiles(designSystemConfig), + ]) + + return registryItemSchema.parse({ + name: designSystemConfig.item ?? "Item", + type: "registry:item", + dependencies: [...(registryBase.dependencies ?? []), "next-themes"], + files: [globalsCss, layoutFile, themeProviderFile, ...componentFiles], + }) +} + +function buildGlobalsCss(registryBase: RegistryItem) { + const lightVars = Object.entries(registryBase.cssVars?.light ?? {}) + .map(([key, value]) => ` --${key}: ${value};`) + .join("\n") + + const darkVars = Object.entries(registryBase.cssVars?.dark ?? {}) + .map(([key, value]) => ` --${key}: ${value};`) + .join("\n") + + const content = dedent`@import "tailwindcss"; +@import "tw-animate-css"; +/* @import "shadcn/tailwind.css"; */ + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + ${THEME_INLINE} } :root { @@ -149,6 +236,7 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { import type { Metadata } from "next"; import { ${font.font.import} } from "next/font/google"; import "./globals.css"; + import { ThemeProvider } from "@/components/theme-provider"; const ${constName} = ${font.font.import}({subsets:['latin'],variable:'${font.font.variable}'}); @@ -163,11 +251,11 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { children: React.ReactNode; }>) { return ( - + - {children} + {children} ); @@ -183,40 +271,47 @@ function buildLayoutFile(designSystemConfig: DesignSystemConfig) { } async function buildComponentFiles(designSystemConfig: DesignSystemConfig) { - const files = [] const allItemsForBase = Object.values(Index[designSystemConfig.base]) .filter((item: RegistryItem) => item.type === "registry:ui") .map((item) => item.name) - // Fetch UI components and the item component in parallel. - const itemComponentPromise = designSystemConfig.item - ? getRegistryItemFile(designSystemConfig.item, designSystemConfig) - : null + // Build config once for all components. + const config = buildTransformConfig(designSystemConfig) - const registryItemFiles = await Promise.all( - allItemsForBase.map((name) => getRegistryItemFile(name, designSystemConfig)) - ) - files.push(...registryItemFiles) + // Fetch UI components and the item component in parallel. + const [registryItemFiles, itemComponentFile] = await Promise.all([ + Promise.all( + allItemsForBase.map((name) => + getRegistryItemFile(name, designSystemConfig, config) + ) + ), + designSystemConfig.item + ? getRegistryItemFile(designSystemConfig.item, designSystemConfig, config) + : null, + ]) + + const files = [...registryItemFiles] const pageFile = { path: "app/page.tsx", type: "registry:page", target: "app/page.tsx", content: dedent` + import { Button } from "@/components/ui/button" + export default function Page() { return (
-
-

Project ready!

-

You may now prompt v0 to start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
+
+ (Press d to toggle dark mode) +
) @@ -225,36 +320,33 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) { } // Build the actual item component. - if (itemComponentPromise) { - const itemComponentFile = await itemComponentPromise - if (itemComponentFile) { - // Find the export default function from the component file. - const exportDefault = itemComponentFile.content.match( - /export default function (\w+)/ + if (itemComponentFile) { + // Find the export default function from the component file. + const exportDefault = itemComponentFile.content.match( + /export default function (\w+)/ + ) + if (exportDefault) { + const functionName = exportDefault[1] + + // Replace the export default function with a named export. + itemComponentFile.content = itemComponentFile.content.replace( + /export default function (\w+)/, + `export function ${functionName}` ) - if (exportDefault) { - const functionName = exportDefault[1] - // Replace the export default function with a named export. - itemComponentFile.content = itemComponentFile.content.replace( - /export default function (\w+)/, - `export function ${functionName}` - ) + // Import and render the item on the page. + pageFile.content = dedent`import { ${functionName} } from "@/components/${designSystemConfig.item}"; - // Import and render the item on the page. - pageFile.content = dedent`import { ${functionName} } from "@/components/${designSystemConfig.item}"; - - export default function Page() { - return <${functionName} /> - }` - } - - files.push({ - ...itemComponentFile, - target: `components/${designSystemConfig.item}.tsx`, - type: "registry:component", - }) + export default function Page() { + return <${functionName} /> + }` } + + files.push({ + ...itemComponentFile, + target: `components/${designSystemConfig.item}.tsx`, + type: "registry:component", + }) } files.push(pageFile) @@ -262,23 +354,8 @@ async function buildComponentFiles(designSystemConfig: DesignSystemConfig) { return z.array(registryItemFileSchema).parse(files) } -async function getRegistryItemFile( - name: string, - designSystemConfig: DesignSystemConfig -) { - const response = await fetch( - `${process.env.NEXT_PUBLIC_APP_URL}/r/styles/${designSystemConfig.base}-${designSystemConfig.style}/${name}.json` - ) - - if (!response.ok) { - throw new Error(`Failed to fetch registry item: ${response.statusText}`) - } - - const json = await response.json() - const item = registryItemSchema.parse(json) - - // Build a v0 config i.e components.json. - const config = { +function buildTransformConfig(designSystemConfig: DesignSystemConfig) { + return { $schema: "https://ui.shadcn.com/schema.json", style: `${designSystemConfig.base}-${designSystemConfig.style}`, rsc: true, @@ -311,6 +388,23 @@ async function getRegistryItemFile( ui: "./components/ui", }, } satisfies z.infer +} + +async function getRegistryItemFile( + name: string, + designSystemConfig: DesignSystemConfig, + config: z.infer +) { + const response = await fetch( + `${process.env.NEXT_PUBLIC_APP_URL}/r/styles/${designSystemConfig.base}-${designSystemConfig.style}/${name}.json` + ) + + if (!response.ok) { + throw new Error(`Failed to fetch registry item: ${response.statusText}`) + } + + const json = await response.json() + const item = registryItemSchema.parse(json) const file = item.files?.[0] if (!file?.content) { @@ -330,11 +424,6 @@ async function getRegistryItemFile( } } -const transformers = [transformIcons, transformMenu, transformRender] - -// Reuse a single ts-morph Project — avoids re-creating the compiler host per file. -const project = new Project({ compilerOptions: {} }) - async function transformFileContent( content: string, config: z.infer diff --git a/apps/v4/public/r/templates/astro-app.tar.gz b/apps/v4/public/r/templates/astro-app.tar.gz index f3cdc0de90..aa7fe0b427 100644 Binary files a/apps/v4/public/r/templates/astro-app.tar.gz and b/apps/v4/public/r/templates/astro-app.tar.gz differ diff --git a/apps/v4/public/r/templates/astro-monorepo.tar.gz b/apps/v4/public/r/templates/astro-monorepo.tar.gz index bf91e16d9c..dc44ccd911 100644 Binary files a/apps/v4/public/r/templates/astro-monorepo.tar.gz and b/apps/v4/public/r/templates/astro-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/next-app.tar.gz b/apps/v4/public/r/templates/next-app.tar.gz index 2fdf3d4dde..30fd006cd5 100644 Binary files a/apps/v4/public/r/templates/next-app.tar.gz and b/apps/v4/public/r/templates/next-app.tar.gz differ diff --git a/apps/v4/public/r/templates/next-monorepo.tar.gz b/apps/v4/public/r/templates/next-monorepo.tar.gz index 3fc668c2a9..14f4a5203f 100644 Binary files a/apps/v4/public/r/templates/next-monorepo.tar.gz and b/apps/v4/public/r/templates/next-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/react-router-app.tar.gz b/apps/v4/public/r/templates/react-router-app.tar.gz index 41207cfd9c..745335011e 100644 Binary files a/apps/v4/public/r/templates/react-router-app.tar.gz and b/apps/v4/public/r/templates/react-router-app.tar.gz differ diff --git a/apps/v4/public/r/templates/react-router-monorepo.tar.gz b/apps/v4/public/r/templates/react-router-monorepo.tar.gz index ef372aad50..51dfd96a6b 100644 Binary files a/apps/v4/public/r/templates/react-router-monorepo.tar.gz and b/apps/v4/public/r/templates/react-router-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/start-app.tar.gz b/apps/v4/public/r/templates/start-app.tar.gz index d0e85235c2..8319d83577 100644 Binary files a/apps/v4/public/r/templates/start-app.tar.gz and b/apps/v4/public/r/templates/start-app.tar.gz differ diff --git a/apps/v4/public/r/templates/start-monorepo.tar.gz b/apps/v4/public/r/templates/start-monorepo.tar.gz index 63b7a0f1ad..f564b0ab5d 100644 Binary files a/apps/v4/public/r/templates/start-monorepo.tar.gz and b/apps/v4/public/r/templates/start-monorepo.tar.gz differ diff --git a/apps/v4/public/r/templates/vite-app.tar.gz b/apps/v4/public/r/templates/vite-app.tar.gz index a6a129e64e..d342c81b94 100644 Binary files a/apps/v4/public/r/templates/vite-app.tar.gz and b/apps/v4/public/r/templates/vite-app.tar.gz differ diff --git a/apps/v4/public/r/templates/vite-monorepo.tar.gz b/apps/v4/public/r/templates/vite-monorepo.tar.gz index 221b3eeb81..2416cd2d52 100644 Binary files a/apps/v4/public/r/templates/vite-monorepo.tar.gz and b/apps/v4/public/r/templates/vite-monorepo.tar.gz differ diff --git a/packages/shadcn/src/commands/init.ts b/packages/shadcn/src/commands/init.ts index c3e260ddb8..3a0e9da9bd 100644 --- a/packages/shadcn/src/commands/init.ts +++ b/packages/shadcn/src/commands/init.ts @@ -592,6 +592,8 @@ export async function runInit( const components = [ ...(options.installStyleIndex ? ["index"] : []), ...(options.components ?? []), + // Add button component for new template-based projects. + ...(selectedTemplate ? ["button"] : []), ] if (selectedTemplate?.init) { diff --git a/templates/astro-app/src/pages/index.astro b/templates/astro-app/src/pages/index.astro index 547b92ceab..ad15f2523e 100644 --- a/templates/astro-app/src/pages/index.astro +++ b/templates/astro-app/src/pages/index.astro @@ -1,20 +1,17 @@ --- import Layout from "@/layouts/main.astro" +import { Button } from "@/components/ui/button" ---
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
diff --git a/templates/astro-monorepo/apps/web/src/pages/index.astro b/templates/astro-monorepo/apps/web/src/pages/index.astro index ea13d31fa4..a14300aacc 100644 --- a/templates/astro-monorepo/apps/web/src/pages/index.astro +++ b/templates/astro-monorepo/apps/web/src/pages/index.astro @@ -1,5 +1,6 @@ --- import "@workspace/ui/globals.css" +import { Button } from "@workspace/ui/components/ui/button" --- @@ -11,17 +12,13 @@ import "@workspace/ui/globals.css"
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
diff --git a/templates/next-app/app/page.tsx b/templates/next-app/app/page.tsx index b2994dbc7b..98ff035b02 100644 --- a/templates/next-app/app/page.tsx +++ b/templates/next-app/app/page.tsx @@ -1,17 +1,18 @@ +import { Button } from "@/components/ui/button" + export default function Page() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
+
+ (Press d to toggle dark mode) +
) diff --git a/templates/next-monorepo/apps/web/app/page.tsx b/templates/next-monorepo/apps/web/app/page.tsx index b488a0eb36..8231c83fb9 100644 --- a/templates/next-monorepo/apps/web/app/page.tsx +++ b/templates/next-monorepo/apps/web/app/page.tsx @@ -1,17 +1,18 @@ +import { Button } from "@workspace/ui/components/ui/button" + export default function Page() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
+
+ (Press d to toggle dark mode) +
) diff --git a/templates/react-router-app/app/routes/home.tsx b/templates/react-router-app/app/routes/home.tsx index f31d2f39fe..938c95e082 100644 --- a/templates/react-router-app/app/routes/home.tsx +++ b/templates/react-router-app/app/routes/home.tsx @@ -1,17 +1,15 @@ +import { Button } from "@/components/ui/button" + export default function Home() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
) diff --git a/templates/react-router-monorepo/apps/web/app/routes/home.tsx b/templates/react-router-monorepo/apps/web/app/routes/home.tsx index f31d2f39fe..c4b7fe6d30 100644 --- a/templates/react-router-monorepo/apps/web/app/routes/home.tsx +++ b/templates/react-router-monorepo/apps/web/app/routes/home.tsx @@ -1,17 +1,15 @@ +import { Button } from "@workspace/ui/components/ui/button" + export default function Home() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
) diff --git a/templates/start-app/src/routes/index.tsx b/templates/start-app/src/routes/index.tsx index 854dfa4709..7933a4992c 100644 --- a/templates/start-app/src/routes/index.tsx +++ b/templates/start-app/src/routes/index.tsx @@ -1,21 +1,18 @@ import { createFileRoute } from "@tanstack/react-router" +import { Button } from "@/components/ui/button" export const Route = createFileRoute("/")({ component: App }) function App() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
) diff --git a/templates/start-monorepo/apps/web/src/routes/index.tsx b/templates/start-monorepo/apps/web/src/routes/index.tsx index 854dfa4709..806cdde062 100644 --- a/templates/start-monorepo/apps/web/src/routes/index.tsx +++ b/templates/start-monorepo/apps/web/src/routes/index.tsx @@ -1,21 +1,18 @@ import { createFileRoute } from "@tanstack/react-router" +import { Button } from "@workspace/ui/components/ui/button" export const Route = createFileRoute("/")({ component: App }) function App() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
) diff --git a/templates/vite-app/src/App.tsx b/templates/vite-app/src/App.tsx index 5b1a654e2d..4fddc488ff 100644 --- a/templates/vite-app/src/App.tsx +++ b/templates/vite-app/src/App.tsx @@ -1,17 +1,18 @@ +import { Button } from "@/components/ui/button" + export function App() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
+
+ (Press d to toggle dark mode) +
) diff --git a/templates/vite-monorepo/apps/web/src/App.tsx b/templates/vite-monorepo/apps/web/src/App.tsx index ba588acfab..9e8103760b 100644 --- a/templates/vite-monorepo/apps/web/src/App.tsx +++ b/templates/vite-monorepo/apps/web/src/App.tsx @@ -1,17 +1,18 @@ +import { Button } from "@workspace/ui/components/ui/button" + export function App() { return (
-
-

Project ready!

-

You may now add components and start building.

- - Read the docs - +
+
+

Project ready!

+

You may now add components and start building.

+

We've already added the button component for you.

+ +
+
+ (Press d to toggle dark mode) +
)