mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat: wip for init
This commit is contained in:
@@ -68,7 +68,7 @@ export function TemplatePicker({
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
template: value as "next" | "next-monorepo" | "start" | "vite",
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
ComputerTerminal01Icon,
|
||||
Copy01Icon,
|
||||
Tick02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import * as React from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -41,12 +41,6 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const TEMPLATES = [
|
||||
{
|
||||
@@ -54,6 +48,11 @@ const TEMPLATES = [
|
||||
title: "Next.js",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" fill="currentColor"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "next-monorepo",
|
||||
title: "Next.js (Monorepo)",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" fill="currentColor"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "start",
|
||||
title: "TanStack Start",
|
||||
@@ -180,10 +179,10 @@ export function ToolbarControls() {
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
template: value as "next" | "next-monorepo" | "start" | "vite",
|
||||
})
|
||||
}}
|
||||
className="grid grid-cols-3 gap-2"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((template) => (
|
||||
<FieldLabel
|
||||
@@ -217,7 +216,7 @@ export function ToolbarControls() {
|
||||
<FieldTitle>Enable RTL</FieldTitle>
|
||||
<FieldDescription>
|
||||
<a
|
||||
href={`/docs/rtl/${params.template}`}
|
||||
href={`/docs/rtl/${params.template === "next-monorepo" ? "next" : params.template}`}
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -243,33 +242,26 @@ export function ToolbarControls() {
|
||||
}}
|
||||
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<div className="flex items-center gap-2 px-1.5 py-1">
|
||||
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
|
||||
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
|
||||
<TabsTrigger value="npm">npm</TabsTrigger>
|
||||
<TabsTrigger value="yarn">yarn</TabsTrigger>
|
||||
<TabsTrigger value="bun">bun</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopyFromTabs}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasCopied ? "Copied!" : "Copy command"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopyFromTabs}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function GET(request: NextRequest) {
|
||||
menuAccent: searchParams.get("menuAccent"),
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
template: searchParams.get("template"),
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
})
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ const designSystemSearchParams = {
|
||||
).withDefault("default"),
|
||||
template: parseAsStringLiteral([
|
||||
"next",
|
||||
"next-monorepo",
|
||||
"start",
|
||||
"vite",
|
||||
] as const).withDefault("next"),
|
||||
|
||||
@@ -94,7 +94,7 @@ export const designSystemConfigSchema = z
|
||||
radius: z
|
||||
.enum(RADII.map((r) => r.name) as [RadiusValue, ...RadiusValue[]])
|
||||
.default("default"),
|
||||
template: z.enum(["next", "start", "vite"]).default("next").optional(),
|
||||
template: z.enum(["next", "next-monorepo", "start", "vite"]).default("next").optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -433,7 +433,7 @@ export function buildRegistryBase(config: DesignSystemConfig) {
|
||||
},
|
||||
},
|
||||
...(config.rtl && {
|
||||
docs: `To learn how to set up the RTL provider and fonts for your app, see https://ui.shadcn.com/docs/rtl/${config.template ?? "next"}`,
|
||||
docs: `To learn how to set up the RTL provider and fonts for your app, see https://ui.shadcn.com/docs/rtl/${config.template === "next-monorepo" ? "next" : (config.template ?? "next")}`,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clean": "rimraf dist && rimraf components",
|
||||
"start:dev": "cross-env REGISTRY_URL=http://localhost:4000/r node dist/index.js",
|
||||
"start:dev": "cross-env REGISTRY_URL=http://localhost:4000/r SHADCN_TEMPLATE_DIR=../../templates node dist/index.js",
|
||||
"start:prod": "cross-env REGISTRY_URL=https://ui.shadcn.com/r node dist/index.js",
|
||||
"start": "node dist/index.js",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
|
||||
@@ -71,7 +71,7 @@
|
||||
"pub:next": "pnpm build && pnpm publish --no-git-checks --access public --tag next",
|
||||
"pub:release": "pnpm build && pnpm publish --access public",
|
||||
"test": "vitest run",
|
||||
"test:dev": "REGISTRY_URL=http://localhost:4000/r vitest run",
|
||||
"test:dev": "REGISTRY_URL=http://localhost:4000/r SHADCN_TEMPLATE_DIR=../../templates vitest run",
|
||||
"mcp:inspect": "pnpm dlx @modelcontextprotocol/inspector node dist/index.js mcp"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { initOptionsSchema, runInit } from "./init"
|
||||
|
||||
const CREATE_TEMPLATES = {
|
||||
next: "Next.js",
|
||||
"next-monorepo": "Next.js (Monorepo)",
|
||||
vite: "Vite",
|
||||
start: "TanStack Start",
|
||||
} as const
|
||||
@@ -35,7 +36,7 @@ export const create = new Command()
|
||||
.argument("[name]", "the name of your project")
|
||||
.option(
|
||||
"-t, --template <template>",
|
||||
"the template to use. e.g. next, start or vite"
|
||||
"the template to use. e.g. next, next-monorepo, start or vite"
|
||||
)
|
||||
.option("-p, --preset [name]", "use a preset configuration")
|
||||
.option(
|
||||
@@ -265,6 +266,7 @@ export default App;
|
||||
},
|
||||
]
|
||||
case "next":
|
||||
case "next-monorepo":
|
||||
return [
|
||||
{
|
||||
type: "registry:page" as const,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { clearRegistryContext } from "@/src/registry/context"
|
||||
import { rawConfigSchema } from "@/src/schema"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
||||
import { initMonorepoProject } from "@/src/utils/init-monorepo"
|
||||
import { loadEnvFiles } from "@/src/utils/env-loader"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
import {
|
||||
@@ -398,8 +399,35 @@ export async function runInit(
|
||||
}
|
||||
|
||||
if (newProjectTemplate === "next-monorepo") {
|
||||
options.cwd = path.resolve(options.cwd, "apps/web")
|
||||
return await getConfig(options.cwd)
|
||||
// Prompt for base color if not set.
|
||||
if (!options.baseColor) {
|
||||
const baseColors = await getRegistryBaseColors()
|
||||
const { tailwindBaseColor } = await prompts({
|
||||
type: "select",
|
||||
name: "tailwindBaseColor",
|
||||
message: "Which color would you like to use as the base color?",
|
||||
choices: baseColors.map((color) => ({
|
||||
title: color.label,
|
||||
value: color.name,
|
||||
})),
|
||||
})
|
||||
options.baseColor = tailwindBaseColor
|
||||
}
|
||||
|
||||
const components = [
|
||||
...(options.baseStyle ? ["index"] : []),
|
||||
...(options.components ?? []),
|
||||
]
|
||||
|
||||
return await initMonorepoProject({
|
||||
projectPath: options.cwd,
|
||||
components,
|
||||
baseStyle: options.baseStyle,
|
||||
baseColor: options.baseColor ?? "neutral",
|
||||
registryBaseConfig: options.registryBaseConfig,
|
||||
rtl: options.rtl ?? false,
|
||||
silent: options.silent,
|
||||
})
|
||||
}
|
||||
|
||||
const projectConfig = await getProjectConfig(options.cwd, projectInfo)
|
||||
|
||||
@@ -317,56 +317,58 @@ async function addWorkspaceComponents(
|
||||
|
||||
rootSpinner?.succeed()
|
||||
|
||||
// Sort files.
|
||||
filesCreated.sort()
|
||||
filesUpdated.sort()
|
||||
filesSkipped.sort()
|
||||
// Deduplicate and sort files.
|
||||
const dedupedCreated = Array.from(new Set(filesCreated)).sort()
|
||||
const dedupedUpdated = Array.from(
|
||||
new Set(filesUpdated.filter((file) => !filesCreated.includes(file)))
|
||||
).sort()
|
||||
const dedupedSkipped = Array.from(new Set(filesSkipped)).sort()
|
||||
|
||||
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
|
||||
if (!hasUpdatedFiles && !filesSkipped.length) {
|
||||
const hasUpdatedFiles = dedupedCreated.length || dedupedUpdated.length
|
||||
if (!hasUpdatedFiles && !dedupedSkipped.length) {
|
||||
spinner(`No files updated.`, {
|
||||
silent: options.silent,
|
||||
})?.info()
|
||||
}
|
||||
|
||||
if (filesCreated.length) {
|
||||
if (dedupedCreated.length) {
|
||||
spinner(
|
||||
`Created ${filesCreated.length} ${
|
||||
filesCreated.length === 1 ? "file" : "files"
|
||||
`Created ${dedupedCreated.length} ${
|
||||
dedupedCreated.length === 1 ? "file" : "files"
|
||||
}:`,
|
||||
{
|
||||
silent: options.silent,
|
||||
}
|
||||
)?.succeed()
|
||||
for (const file of filesCreated) {
|
||||
for (const file of dedupedCreated) {
|
||||
logger.log(` - ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (filesUpdated.length) {
|
||||
if (dedupedUpdated.length) {
|
||||
spinner(
|
||||
`Updated ${filesUpdated.length} ${
|
||||
filesUpdated.length === 1 ? "file" : "files"
|
||||
`Updated ${dedupedUpdated.length} ${
|
||||
dedupedUpdated.length === 1 ? "file" : "files"
|
||||
}:`,
|
||||
{
|
||||
silent: options.silent,
|
||||
}
|
||||
)?.info()
|
||||
for (const file of filesUpdated) {
|
||||
for (const file of dedupedUpdated) {
|
||||
logger.log(` - ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (filesSkipped.length) {
|
||||
if (dedupedSkipped.length) {
|
||||
spinner(
|
||||
`Skipped ${filesSkipped.length} ${
|
||||
filesUpdated.length === 1 ? "file" : "files"
|
||||
`Skipped ${dedupedSkipped.length} ${
|
||||
dedupedSkipped.length === 1 ? "file" : "files"
|
||||
}: (use --overwrite to overwrite)`,
|
||||
{
|
||||
silent: options.silent,
|
||||
}
|
||||
)?.info()
|
||||
for (const file of filesSkipped) {
|
||||
for (const file of dedupedSkipped) {
|
||||
logger.log(` - ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,36 +237,51 @@ async function createMonorepoProject(
|
||||
).start()
|
||||
|
||||
try {
|
||||
// Get the template.
|
||||
const templatePath = path.join(os.tmpdir(), `shadcn-template-${Date.now()}`)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
const localTemplateDir = process.env.SHADCN_TEMPLATE_DIR
|
||||
if (localTemplateDir) {
|
||||
// Use local template directory for development.
|
||||
const localTemplatePath = path.resolve(localTemplateDir, "monorepo-next")
|
||||
await fs.copy(localTemplatePath, projectPath, {
|
||||
filter: (src) => !src.includes("node_modules"),
|
||||
})
|
||||
} else {
|
||||
// Get the template from GitHub.
|
||||
const templatePath = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-template-${Date.now()}`
|
||||
)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Write the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/monorepo-next",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "monorepo-next")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Write the tar file
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/monorepo-next",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "monorepo-next")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
|
||||
// Run install.
|
||||
// Run install. Disable frozen lockfile since the template's lockfile may not match.
|
||||
await execa(options.packageManager, ["install"], {
|
||||
cwd: projectPath,
|
||||
env: {
|
||||
...process.env,
|
||||
CI: "",
|
||||
},
|
||||
})
|
||||
// await execa("cd", [cwd])
|
||||
|
||||
// Write project name to the package.json
|
||||
// Write project name to the package.json.
|
||||
const packageJsonPath = path.join(projectPath, "package.json")
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8")
|
||||
@@ -302,28 +317,40 @@ async function createViteProject(
|
||||
).start()
|
||||
|
||||
try {
|
||||
// Get the template.
|
||||
const templatePath = path.join(os.tmpdir(), `shadcn-template-${Date.now()}`)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
const localTemplateDir = process.env.SHADCN_TEMPLATE_DIR
|
||||
if (localTemplateDir) {
|
||||
// Use local template directory for development.
|
||||
const localTemplatePath = path.resolve(localTemplateDir, "vite-app")
|
||||
await fs.copy(localTemplatePath, projectPath, {
|
||||
filter: (src) => !src.includes("node_modules"),
|
||||
})
|
||||
} else {
|
||||
// Get the template from GitHub.
|
||||
const templatePath = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-template-${Date.now()}`
|
||||
)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Write the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/vite-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "vite-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
// Write the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/vite-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "vite-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Remove pnpm-lock.yaml if using a different package manager.
|
||||
if (options.packageManager !== "pnpm") {
|
||||
@@ -373,28 +400,40 @@ async function createStartProject(
|
||||
).start()
|
||||
|
||||
try {
|
||||
// Get the template.
|
||||
const templatePath = path.join(os.tmpdir(), `shadcn-template-${Date.now()}`)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
const localTemplateDir = process.env.SHADCN_TEMPLATE_DIR
|
||||
if (localTemplateDir) {
|
||||
// Use local template directory for development.
|
||||
const localTemplatePath = path.resolve(localTemplateDir, "start-app")
|
||||
await fs.copy(localTemplatePath, projectPath, {
|
||||
filter: (src) => !src.includes("node_modules"),
|
||||
})
|
||||
} else {
|
||||
// Get the template from GitHub.
|
||||
const templatePath = path.join(
|
||||
os.tmpdir(),
|
||||
`shadcn-template-${Date.now()}`
|
||||
)
|
||||
await fs.ensureDir(templatePath)
|
||||
const response = await fetch(GITHUB_TEMPLATE_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download template: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Write the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/start-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "start-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
// Write the tar file.
|
||||
const tarPath = path.resolve(templatePath, "template.tar.gz")
|
||||
await fs.writeFile(tarPath, Buffer.from(await response.arrayBuffer()))
|
||||
await execa("tar", [
|
||||
"-xzf",
|
||||
tarPath,
|
||||
"-C",
|
||||
templatePath,
|
||||
"--strip-components=2",
|
||||
"ui-main/templates/start-app",
|
||||
])
|
||||
const extractedPath = path.resolve(templatePath, "start-app")
|
||||
await fs.move(extractedPath, projectPath)
|
||||
await fs.remove(templatePath)
|
||||
}
|
||||
|
||||
// Remove pnpm-lock.yaml if using a different package manager.
|
||||
if (options.packageManager !== "pnpm") {
|
||||
|
||||
116
packages/shadcn/src/utils/init-monorepo.ts
Normal file
116
packages/shadcn/src/utils/init-monorepo.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import path from "path"
|
||||
import { iconLibraries, type IconLibraryName } from "@/src/icons/libraries"
|
||||
import { configWithDefaults } from "@/src/registry/config"
|
||||
import { resolveRegistryTree } from "@/src/registry/resolver"
|
||||
import { rawConfigSchema } from "@/src/schema"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
import { resolveConfigPaths } from "@/src/utils/get-config"
|
||||
import { ensureRegistriesInConfig } from "@/src/utils/registries"
|
||||
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
|
||||
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
||||
import { updateFonts } from "@/src/utils/updaters/update-fonts"
|
||||
import deepmerge from "deepmerge"
|
||||
import fsExtra from "fs-extra"
|
||||
|
||||
export async function initMonorepoProject(options: {
|
||||
projectPath: string
|
||||
components: string[]
|
||||
baseStyle: boolean
|
||||
baseColor: string
|
||||
registryBaseConfig?: Record<string, unknown>
|
||||
rtl: boolean
|
||||
silent: boolean
|
||||
}) {
|
||||
const packagesUiPath = path.resolve(options.projectPath, "packages/ui")
|
||||
const appsWebPath = path.resolve(options.projectPath, "apps/web")
|
||||
|
||||
// Update packages/ui/components.json.
|
||||
const packagesUiConfigPath = path.resolve(packagesUiPath, "components.json")
|
||||
let packagesUiConfig = await fsExtra.readJson(packagesUiConfigPath)
|
||||
if (options.registryBaseConfig) {
|
||||
packagesUiConfig = deepmerge(packagesUiConfig, options.registryBaseConfig)
|
||||
}
|
||||
packagesUiConfig.tailwind.baseColor = options.baseColor
|
||||
if (options.rtl) {
|
||||
packagesUiConfig.rtl = true
|
||||
}
|
||||
await fsExtra.writeJson(packagesUiConfigPath, packagesUiConfig, {
|
||||
spaces: 2,
|
||||
})
|
||||
|
||||
// Update apps/web/components.json.
|
||||
const appsWebConfigPath = path.resolve(appsWebPath, "components.json")
|
||||
let appsWebConfig = await fsExtra.readJson(appsWebConfigPath)
|
||||
if (options.registryBaseConfig) {
|
||||
appsWebConfig = deepmerge(appsWebConfig, options.registryBaseConfig)
|
||||
}
|
||||
appsWebConfig.tailwind.baseColor = options.baseColor
|
||||
if (options.rtl) {
|
||||
appsWebConfig.rtl = true
|
||||
}
|
||||
await fsExtra.writeJson(appsWebConfigPath, appsWebConfig, { spaces: 2 })
|
||||
|
||||
// Apply preset CSS/style to packages/ui directly.
|
||||
// We use the packages/ui config (not apps/web) so addProjectComponents runs
|
||||
// instead of addWorkspaceComponents. This keeps CSS/deps in packages/ui.
|
||||
const resolvedPackagesUiConfig = await resolveConfigPaths(
|
||||
packagesUiPath,
|
||||
rawConfigSchema.parse(packagesUiConfig)
|
||||
)
|
||||
const { config: packagesUiWithRegistries } = await ensureRegistriesInConfig(
|
||||
options.components,
|
||||
resolvedPackagesUiConfig,
|
||||
{ silent: true }
|
||||
)
|
||||
await addComponents(options.components, packagesUiWithRegistries, {
|
||||
overwrite: true,
|
||||
silent: options.silent,
|
||||
isNewProject: true,
|
||||
})
|
||||
|
||||
const resolvedAppsWebConfig = await resolveConfigPaths(
|
||||
appsWebPath,
|
||||
rawConfigSchema.parse(appsWebConfig)
|
||||
)
|
||||
|
||||
// Handle fonts at the apps/web level.
|
||||
// packages/ui has no next.config so massageTreeForFonts can't detect Next.js.
|
||||
// We resolve the tree to get fonts, then apply them using the apps/web config
|
||||
// which has next.config and layout.tsx.
|
||||
const tree = await resolveRegistryTree(
|
||||
options.components,
|
||||
configWithDefaults(packagesUiWithRegistries)
|
||||
)
|
||||
if (tree?.fonts?.length) {
|
||||
const [fontSans] = tree.fonts
|
||||
|
||||
// Add font CSS variable to packages/ui CSS (same as massageTreeForFonts for Next.js).
|
||||
await updateCssVars(
|
||||
{ theme: { [fontSans.font.variable]: `var(${fontSans.font.variable})` } },
|
||||
resolvedPackagesUiConfig,
|
||||
{
|
||||
silent: options.silent,
|
||||
overwriteCssVars: false,
|
||||
}
|
||||
)
|
||||
|
||||
// Update layout.tsx in apps/web with the font import and className.
|
||||
await updateFonts(tree.fonts, resolvedAppsWebConfig, {
|
||||
silent: options.silent,
|
||||
})
|
||||
}
|
||||
|
||||
// Install icon library packages in both workspaces.
|
||||
const iconLibrary = resolvedPackagesUiConfig.iconLibrary as IconLibraryName
|
||||
if (iconLibrary && iconLibrary in iconLibraries) {
|
||||
const iconPackages = [...iconLibraries[iconLibrary].packages]
|
||||
await updateDependencies(iconPackages, [], resolvedPackagesUiConfig, {
|
||||
silent: true,
|
||||
})
|
||||
await updateDependencies(iconPackages, [], resolvedAppsWebConfig, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
||||
return resolvedAppsWebConfig
|
||||
}
|
||||
@@ -251,19 +251,18 @@ export async function updateFiles(
|
||||
// Let's update filesUpdated with the updated files.
|
||||
filesUpdated.push(...updatedFiles)
|
||||
|
||||
// If a file is in filesCreated and filesUpdated, we should remove it from filesUpdated.
|
||||
filesUpdated = filesUpdated.filter((file) => !filesCreated.includes(file))
|
||||
// Remove duplicates and filter out files already in filesCreated.
|
||||
filesCreated = Array.from(new Set(filesCreated))
|
||||
filesUpdated = Array.from(
|
||||
new Set(filesUpdated.filter((file) => !filesCreated.includes(file)))
|
||||
)
|
||||
filesSkipped = Array.from(new Set(filesSkipped))
|
||||
|
||||
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
|
||||
if (!hasUpdatedFiles && !filesSkipped.length) {
|
||||
filesCreatedSpinner?.info("No files updated.")
|
||||
}
|
||||
|
||||
// Remove duplicates.
|
||||
filesCreated = Array.from(new Set(filesCreated))
|
||||
filesUpdated = Array.from(new Set(filesUpdated))
|
||||
filesSkipped = Array.from(new Set(filesSkipped))
|
||||
|
||||
if (filesCreated.length) {
|
||||
filesCreatedSpinner?.succeed(
|
||||
`Created ${filesCreated.length} ${
|
||||
|
||||
209
packages/tests/src/tests/create-monorepo.test.ts
Normal file
209
packages/tests/src/tests/create-monorepo.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import path from "path"
|
||||
import fs from "fs-extra"
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { cssHasProperties, npxShadcn } from "../utils/helpers"
|
||||
import { TEMP_DIR } from "../utils/setup"
|
||||
|
||||
// These tests download the monorepo template from GitHub and install dependencies.
|
||||
// They require network access and a running local registry at REGISTRY_URL.
|
||||
describe("shadcn create - next-monorepo", () => {
|
||||
it("should create a monorepo project with preset", async () => {
|
||||
const projectName = `test-monorepo-preset-${process.pid}`
|
||||
|
||||
const result = await npxShadcn(
|
||||
TEMP_DIR,
|
||||
[
|
||||
"create",
|
||||
projectName,
|
||||
"--template",
|
||||
"next-monorepo",
|
||||
"--preset",
|
||||
"radix-nova",
|
||||
],
|
||||
{ timeout: 120000 }
|
||||
)
|
||||
|
||||
const projectPath = path.join(TEMP_DIR, projectName)
|
||||
|
||||
// Verify project structure exists.
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(projectPath, "packages/ui/components.json"))
|
||||
).toBe(true)
|
||||
expect(
|
||||
await fs.pathExists(path.join(projectPath, "apps/web/components.json"))
|
||||
).toBe(true)
|
||||
|
||||
// Verify packages/ui/components.json is updated with preset config.
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
expect(uiConfig.style).toBe("radix-nova")
|
||||
expect(uiConfig.iconLibrary).toBe("hugeicons")
|
||||
expect(uiConfig.tailwind.css).toBe("src/styles/globals.css")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("neutral")
|
||||
expect(uiConfig.tailwind.cssVariables).toBe(true)
|
||||
expect(uiConfig.aliases.components).toBe("@workspace/ui/components")
|
||||
expect(uiConfig.aliases.utils).toBe("@workspace/ui/lib/utils")
|
||||
|
||||
// Verify apps/web/components.json is updated with preset config.
|
||||
const webConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
expect(webConfig.style).toBe("radix-nova")
|
||||
expect(webConfig.iconLibrary).toBe("hugeicons")
|
||||
expect(webConfig.tailwind.css).toBe(
|
||||
"../../packages/ui/src/styles/globals.css"
|
||||
)
|
||||
expect(webConfig.tailwind.baseColor).toBe("neutral")
|
||||
// Verify workspace aliases are preserved.
|
||||
expect(webConfig.aliases.components).toBe("@/components")
|
||||
expect(webConfig.aliases.utils).toBe("@workspace/ui/lib/utils")
|
||||
expect(webConfig.aliases.ui).toBe("@workspace/ui/components")
|
||||
|
||||
// Verify CSS was applied to packages/ui.
|
||||
const cssPath = path.join(projectPath, "packages/ui/src/styles/globals.css")
|
||||
expect(await fs.pathExists(cssPath)).toBe(true)
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain("@layer base")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
expect(cssContent).toContain("--primary")
|
||||
|
||||
// Verify component-example was added to apps/web.
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(projectPath, "apps/web/components/component-example.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
|
||||
// Verify page.tsx was written.
|
||||
const pageContent = await fs.readFile(
|
||||
path.join(projectPath, "apps/web/app/page.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(pageContent).toContain("ComponentExample")
|
||||
expect(pageContent).toContain(
|
||||
'import { ComponentExample } from "@/components/component-example"'
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a monorepo with custom base color and font", async () => {
|
||||
const projectName = `test-monorepo-custom-${process.pid}`
|
||||
|
||||
// Use radix-maia preset which has figtree font and hugeicons.
|
||||
const result = await npxShadcn(
|
||||
TEMP_DIR,
|
||||
[
|
||||
"create",
|
||||
projectName,
|
||||
"--template",
|
||||
"next-monorepo",
|
||||
"--preset",
|
||||
"radix-maia",
|
||||
],
|
||||
{ timeout: 120000 }
|
||||
)
|
||||
|
||||
const projectPath = path.join(TEMP_DIR, projectName)
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
|
||||
// Verify preset config is applied to both components.json files.
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
expect(uiConfig.style).toBe("radix-maia")
|
||||
expect(uiConfig.iconLibrary).toBe("hugeicons")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("neutral")
|
||||
|
||||
const webConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
expect(webConfig.style).toBe("radix-maia")
|
||||
expect(webConfig.iconLibrary).toBe("hugeicons")
|
||||
|
||||
// Verify CSS file has theme variables.
|
||||
const cssPath = path.join(projectPath, "packages/ui/src/styles/globals.css")
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain("@layer base")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
|
||||
// Verify font registry dependency was installed (figtree font).
|
||||
expect(cssContent).toContain("--font-sans")
|
||||
})
|
||||
|
||||
it("should create a monorepo with custom preset url", async () => {
|
||||
const projectName = `test-monorepo-url-${process.pid}`
|
||||
|
||||
// Build a custom init URL with specific options.
|
||||
const registryUrl = process.env.REGISTRY_URL || "http://localhost:4000/r"
|
||||
const baseUrl = registryUrl.replace(/\/r\/?$/, "")
|
||||
const initUrl = `${baseUrl}/init?base=radix&style=nova&baseColor=zinc&theme=zinc&iconLibrary=lucide&font=inter&rtl=false&menuAccent=subtle&menuColor=default&radius=default&template=next`
|
||||
|
||||
const result = await npxShadcn(
|
||||
TEMP_DIR,
|
||||
[
|
||||
"create",
|
||||
projectName,
|
||||
"--template",
|
||||
"next-monorepo",
|
||||
"--preset",
|
||||
initUrl,
|
||||
],
|
||||
{ timeout: 120000 }
|
||||
)
|
||||
|
||||
const projectPath = path.join(TEMP_DIR, projectName)
|
||||
expect(await fs.pathExists(projectPath)).toBe(true)
|
||||
|
||||
// Verify config reflects the custom URL params.
|
||||
const uiConfig = await fs.readJson(
|
||||
path.join(projectPath, "packages/ui/components.json")
|
||||
)
|
||||
expect(uiConfig.style).toBe("radix-nova")
|
||||
expect(uiConfig.iconLibrary).toBe("lucide")
|
||||
expect(uiConfig.tailwind.baseColor).toBe("zinc")
|
||||
|
||||
const webConfig = await fs.readJson(
|
||||
path.join(projectPath, "apps/web/components.json")
|
||||
)
|
||||
expect(webConfig.style).toBe("radix-nova")
|
||||
expect(webConfig.tailwind.baseColor).toBe("zinc")
|
||||
|
||||
// Verify CSS has zinc color theme applied.
|
||||
const cssPath = path.join(projectPath, "packages/ui/src/styles/globals.css")
|
||||
const cssContent = await fs.readFile(cssPath, "utf-8")
|
||||
expect(cssContent).toContain(":root")
|
||||
expect(cssContent).toContain(".dark")
|
||||
expect(cssContent).toContain("--background")
|
||||
expect(cssContent).toContain("--foreground")
|
||||
expect(
|
||||
cssHasProperties(cssContent, [
|
||||
{
|
||||
selector: ":root",
|
||||
properties: {
|
||||
"--background": "oklch(1 0 0)",
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBe(true)
|
||||
|
||||
// Verify component-example and page.tsx.
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(projectPath, "apps/web/components/component-example.tsx")
|
||||
)
|
||||
).toBe(true)
|
||||
const pageContent = await fs.readFile(
|
||||
path.join(projectPath, "apps/web/app/page.tsx"),
|
||||
"utf-8"
|
||||
)
|
||||
expect(pageContent).toContain("ComponentExample")
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import { TEMP_DIR } from "./setup"
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const FIXTURES_DIR = path.join(__dirname, "../../fixtures")
|
||||
const SHADCN_CLI_PATH = path.join(__dirname, "../../../shadcn/dist/index.js")
|
||||
const TEMPLATES_DIR = path.join(__dirname, "../../../../templates")
|
||||
|
||||
export function getRegistryUrl() {
|
||||
return process.env.REGISTRY_URL || "http://localhost:4000/r"
|
||||
@@ -32,6 +33,7 @@ export async function runCommand(
|
||||
options?: {
|
||||
env?: Record<string, string>
|
||||
input?: string
|
||||
timeout?: number
|
||||
}
|
||||
) {
|
||||
try {
|
||||
@@ -45,7 +47,7 @@ export async function runCommand(
|
||||
},
|
||||
input: options?.input,
|
||||
reject: false,
|
||||
timeout: 30000,
|
||||
timeout: options?.timeout ?? 30000,
|
||||
})
|
||||
|
||||
const result = await childProcess
|
||||
@@ -69,14 +71,18 @@ export async function npxShadcn(
|
||||
args: string[],
|
||||
{
|
||||
debug = false,
|
||||
timeout,
|
||||
}: {
|
||||
debug?: boolean
|
||||
timeout?: number
|
||||
} = {}
|
||||
) {
|
||||
const result = await runCommand(cwd, args, {
|
||||
env: {
|
||||
REGISTRY_URL: getRegistryUrl(),
|
||||
SHADCN_TEMPLATE_DIR: TEMPLATES_DIR,
|
||||
},
|
||||
timeout,
|
||||
})
|
||||
|
||||
if (debug) {
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@workspace/ui": "workspace:*",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
|
||||
@@ -7,15 +7,9 @@
|
||||
"lint": "eslint . --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
||||
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-8",
|
||||
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -2,126 +2,3 @@
|
||||
@source "../../../apps/**/*.{ts,tsx}";
|
||||
@source "../../../components/**/*.{ts,tsx}";
|
||||
@source "../**/*.{ts,tsx}";
|
||||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
2670
templates/monorepo-next/pnpm-lock.yaml
generated
2670
templates/monorepo-next/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user