This commit is contained in:
shadcn
2026-03-02 13:57:42 +04:00
parent e85a698821
commit 4d89b13e6f
9 changed files with 432 additions and 59 deletions

View File

@@ -0,0 +1,244 @@
import dedent from "dedent"
import { UI_COMPONENTS } from "@/lib/components"
import {
buildRegistryBase,
fonts,
type DesignSystemConfig,
} from "@/registry/config"
// Builds step-by-step markdown instructions for manually setting up a project.
export function buildInstructions(config: DesignSystemConfig) {
const registryBase = buildRegistryBase(config)
const dependencies = [
...(registryBase.dependencies ?? []),
"clsx",
"tailwind-merge",
]
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 font = fonts.find((f) => f.name === `font-${config.font}`)
const sections = [
buildDependenciesSection(dependencies),
buildUtilsSection(),
buildCssSection(lightVars, darkVars),
buildFontSection(font),
buildComponentsJsonSection(config),
buildAvailableComponentsSection(config),
config.rtl ? buildRtlSection(config) : null,
]
return sections.filter(Boolean).join("\n\n---\n\n")
}
function buildDependenciesSection(dependencies: string[]) {
const list = dependencies.map((dep) => `- ${dep}`).join("\n")
return dedent`
## Step 1: Dependencies
The following dependencies are required:
${list}
`
}
function buildUtilsSection() {
return dedent`
## Step 2: Create \`lib/utils.ts\`
\`\`\`ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
\`\`\`
`
}
function buildCssSection(lightVars: string, darkVars: string) {
return dedent`
## Step 3: Set up CSS
Add the following to your global CSS file (e.g. \`app/globals.css\`):
\`\`\`css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@theme inline {
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--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);
--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);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--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);
}
:root {
${lightVars}
}
.dark {
${darkVars}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
\`\`\`
`
}
function buildFontSection(font: (typeof fonts)[number] | undefined) {
if (!font) {
return null
}
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
return dedent`
## Step 4: Set up the font
This config uses **${font.title}** (\`${font.font.variable}\`).
### Next.js
\`\`\`tsx
import { ${font.font.import} } from "next/font/google"
const fontSans = ${font.font.import}({
subsets: ["latin"],
variable: "${font.font.variable}",
})
// Add fontSans.variable to your <html> className.
// <html className={fontSans.variable}>
\`\`\`
### Other frameworks
Add the font from [Google Fonts](${googleFontsUrl}) and set the \`${font.font.variable}\` CSS variable to the font family:
\`\`\`css
:root {
${font.font.variable}: ${font.font.family};
}
\`\`\`
`
}
function buildComponentsJsonSection(config: DesignSystemConfig) {
const componentsJson = {
$schema: "https://ui.shadcn.com/schema.json",
style: `${config.base}-${config.style}`,
tailwind: {
css: "app/globals.css",
baseColor: config.baseColor,
},
iconLibrary: config.iconLibrary,
aliases: {
components: "@/components",
utils: "@/lib/utils",
ui: "@/components/ui",
lib: "@/lib",
hooks: "@/hooks",
},
}
return dedent`
## Step 5: Create \`components.json\`
Add a \`components.json\` file to the root of your project:
\`\`\`json
${JSON.stringify(componentsJson, null, 2)}
\`\`\`
`
}
function buildAvailableComponentsSection(config: DesignSystemConfig) {
const list = UI_COMPONENTS.join(", ")
const style = `${config.base}-${config.style}`
return dedent`
## Available Components
${list}
To fetch the source for a component, use:
\`https://ui.shadcn.com/r/styles/${style}/<component>.json\`
For documentation and examples, visit:
\`https://ui.shadcn.com/docs/components/${config.base}/<component>\`
`
}
function buildRtlSection(config: DesignSystemConfig) {
const template =
config.template === "next-monorepo" ? "next" : (config.template ?? "next")
return dedent`
## RTL Support
Add \`dir="rtl"\` to your root \`<html>\` element:
\`\`\`html
<html dir="rtl">
\`\`\`
For full RTL setup including the \`DirectionProvider\`, see the [RTL documentation](https://ui.shadcn.com/docs/rtl/${template}).
`
}

View File

@@ -0,0 +1,30 @@
import { type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
import { buildInstructions } from "./build-instructions"
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const result = parseDesignSystemConfig(searchParams)
if (!result.success) {
return new Response(result.error, { status: 400 })
}
track("create_app_manual", result.data)
const markdown = buildInstructions(result.data)
return new Response(markdown, {
headers: { "Content-Type": "text/markdown; charset=utf-8" },
})
} catch (error) {
return new Response(
error instanceof Error ? error.message : "An unknown error occurred",
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,47 @@
import { decodePreset, isPresetCode } from "shadcn/preset"
import {
designSystemConfigSchema,
type DesignSystemConfig,
} from "@/registry/config"
// Parses design system config from URL search params.
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
let configInput: Record<string, unknown>
const presetParam = searchParams.get("preset")
if (presetParam && isPresetCode(presetParam)) {
const decoded = decodePreset(presetParam)
if (!decoded) {
return { success: false as const, error: "Invalid preset code" }
}
configInput = {
...decoded,
base: searchParams.get("base") ?? "radix",
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
}
} else {
configInput = {
base: searchParams.get("base"),
style: searchParams.get("style"),
iconLibrary: searchParams.get("iconLibrary"),
baseColor: searchParams.get("baseColor"),
theme: searchParams.get("theme"),
font: searchParams.get("font"),
menuAccent: searchParams.get("menuAccent"),
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
}
}
const result = designSystemConfigSchema.safeParse(configInput)
if (!result.success) {
return { success: false as const, error: result.error.issues[0].message }
}
return { success: true as const, data: result.data as DesignSystemConfig }
}

View File

@@ -1,54 +1,17 @@
import { NextResponse, type NextRequest } from "next/server"
import { track } from "@vercel/analytics/server"
import { decodePreset, isPresetCode } from "shadcn/preset"
import { registryItemSchema } from "shadcn/schema"
import { buildRegistryBase, designSystemConfigSchema } from "@/registry/config"
import { buildRegistryBase } from "@/registry/config"
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
// Decode preset code if present.
let configInput: Record<string, unknown>
const presetParam = searchParams.get("preset")
if (presetParam && isPresetCode(presetParam)) {
const decoded = decodePreset(presetParam)
if (!decoded) {
return NextResponse.json(
{ error: "Invalid preset code" },
{ status: 400 }
)
}
configInput = {
...decoded,
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
}
} else {
configInput = {
base: searchParams.get("base"),
style: searchParams.get("style"),
iconLibrary: searchParams.get("iconLibrary"),
baseColor: searchParams.get("baseColor"),
theme: searchParams.get("theme"),
font: searchParams.get("font"),
menuAccent: searchParams.get("menuAccent"),
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template") ?? undefined,
rtl: searchParams.get("rtl") === "true",
}
}
const result = designSystemConfigSchema.safeParse(configInput)
const result = parseDesignSystemConfig(searchParams)
if (!result.success) {
return NextResponse.json(
{ error: result.error.issues[0].message },
{ status: 400 }
)
return NextResponse.json({ error: result.error }, { status: 400 })
}
const registryBase = buildRegistryBase(result.data)

View File

@@ -8,12 +8,14 @@ import {
Lora,
Merriweather,
Noto_Sans,
Noto_Serif,
Nunito_Sans,
Outfit,
Playfair_Display,
Public_Sans,
Raleway,
Roboto,
Roboto_Slab,
} from "next/font/google"
const inter = Inter({
@@ -76,9 +78,14 @@ const outfit = Outfit({
variable: "--font-outfit",
})
const lora = Lora({
const notoSerif = Noto_Serif({
subsets: ["latin"],
variable: "--font-lora",
variable: "--font-noto-serif",
})
const robotoSlab = Roboto_Slab({
subsets: ["latin"],
variable: "--font-roboto-slab",
})
const merriweather = Merriweather({
@@ -86,6 +93,11 @@ const merriweather = Merriweather({
variable: "--font-merriweather",
})
const lora = Lora({
subsets: ["latin"],
variable: "--font-lora",
})
const playfairDisplay = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair-display",
@@ -165,9 +177,15 @@ export const FONTS = [
type: "mono",
},
{
name: "Lora",
value: "lora",
font: lora,
name: "Noto Serif",
value: "noto-serif",
font: notoSerif,
type: "serif",
},
{
name: "Roboto Slab",
value: "roboto-slab",
font: robotoSlab,
type: "serif",
},
{
@@ -176,6 +194,12 @@ export const FONTS = [
font: merriweather,
type: "serif",
},
{
name: "Lora",
value: "lora",
font: lora,
type: "serif",
},
{
name: "Playfair Display",
value: "playfair-display",

65
apps/v4/lib/components.ts Normal file
View File

@@ -0,0 +1,65 @@
// All available UI components.
export const UI_COMPONENTS = [
"accordion",
"alert",
"alert-dialog",
"aspect-ratio",
"avatar",
"badge",
"breadcrumb",
"button",
"button-group",
"calendar",
"card",
"carousel",
"chart",
"checkbox",
"collapsible",
"combobox",
"command",
"context-menu",
"data-table",
"date-picker",
"dialog",
"direction",
"drawer",
"dropdown-menu",
"empty",
"field",
"form",
"hover-card",
"input",
"input-group",
"input-otp",
"item",
"kbd",
"label",
"menubar",
"native-select",
"navigation-menu",
"pagination",
"popover",
"progress",
"radio-group",
"resizable",
"scroll-area",
"select",
"separator",
"sheet",
"sidebar",
"skeleton",
"slider",
"sonner",
"spinner",
"switch",
"table",
"tabs",
"textarea",
"toast",
"toggle",
"toggle-group",
"tooltip",
"typography",
] as const
export type UIComponent = (typeof UI_COMPONENTS)[number]

View File

@@ -121,6 +121,10 @@ const nextConfig = {
source: "/docs/:path*.md",
destination: "/llm/:path*",
},
{
source: "/init.md",
destination: "/init/md",
},
]
},
}

View File

@@ -98,7 +98,7 @@
"eslint": "^9",
"eslint-config-next": "16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.11",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5",
@@ -137,6 +137,8 @@
"jsx",
"decorators-legacy"
],
"tailwindStylesheet": "./styles/globals.css",
"tailwindFunctions": ["cn", "cva"],
"plugins": [
"@ianvs/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss"

18
pnpm-lock.yaml generated
View File

@@ -336,8 +336,8 @@ importers:
specifier: ^3.4.2
version: 3.6.2
prettier-plugin-tailwindcss:
specifier: ^0.6.11
version: 0.6.14(@ianvs/prettier-plugin-sort-imports@4.6.1(prettier@3.6.2))(prettier@3.6.2)
specifier: ^0.7.2
version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.6.1(prettier@3.6.2))(prettier@3.6.2)
tailwindcss:
specifier: ^4
version: 4.1.18
@@ -6511,9 +6511,9 @@ packages:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'}
prettier-plugin-tailwindcss@0.6.14:
resolution: {integrity: sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==}
engines: {node: '>=14.21.3'}
prettier-plugin-tailwindcss@0.7.2:
resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==}
engines: {node: '>=20.19'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
'@prettier/plugin-hermes': '*'
@@ -6525,14 +6525,12 @@ packages:
prettier: ^3.0
prettier-plugin-astro: '*'
prettier-plugin-css-order: '*'
prettier-plugin-import-sort: '*'
prettier-plugin-jsdoc: '*'
prettier-plugin-marko: '*'
prettier-plugin-multiline-arrays: '*'
prettier-plugin-organize-attributes: '*'
prettier-plugin-organize-imports: '*'
prettier-plugin-sort-imports: '*'
prettier-plugin-style-order: '*'
prettier-plugin-svelte: '*'
peerDependenciesMeta:
'@ianvs/prettier-plugin-sort-imports':
@@ -6553,8 +6551,6 @@ packages:
optional: true
prettier-plugin-css-order:
optional: true
prettier-plugin-import-sort:
optional: true
prettier-plugin-jsdoc:
optional: true
prettier-plugin-marko:
@@ -6567,8 +6563,6 @@ packages:
optional: true
prettier-plugin-sort-imports:
optional: true
prettier-plugin-style-order:
optional: true
prettier-plugin-svelte:
optional: true
@@ -14745,7 +14739,7 @@ snapshots:
prepend-http@2.0.0: {}
prettier-plugin-tailwindcss@0.6.14(@ianvs/prettier-plugin-sort-imports@4.6.1(prettier@3.6.2))(prettier@3.6.2):
prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.6.1(prettier@3.6.2))(prettier@3.6.2):
dependencies:
prettier: 3.6.2
optionalDependencies: