From 4d89b13e6ff4dc254d12a40a1ad556c09a751fa8 Mon Sep 17 00:00:00 2001 From: shadcn Date: Mon, 2 Mar 2026 13:57:42 +0400 Subject: [PATCH] fix --- .../(create)/init/md/build-instructions.ts | 244 ++++++++++++++++++ apps/v4/app/(create)/init/md/route.ts | 30 +++ apps/v4/app/(create)/init/parse-config.ts | 47 ++++ apps/v4/app/(create)/init/route.ts | 45 +--- apps/v4/app/(create)/lib/fonts.ts | 34 ++- apps/v4/lib/components.ts | 65 +++++ apps/v4/next.config.mjs | 4 + apps/v4/package.json | 4 +- pnpm-lock.yaml | 18 +- 9 files changed, 432 insertions(+), 59 deletions(-) create mode 100644 apps/v4/app/(create)/init/md/build-instructions.ts create mode 100644 apps/v4/app/(create)/init/md/route.ts create mode 100644 apps/v4/app/(create)/init/parse-config.ts create mode 100644 apps/v4/lib/components.ts diff --git a/apps/v4/app/(create)/init/md/build-instructions.ts b/apps/v4/app/(create)/init/md/build-instructions.ts new file mode 100644 index 0000000000..45d870fa98 --- /dev/null +++ b/apps/v4/app/(create)/init/md/build-instructions.ts @@ -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 className. + // + \`\`\` + + ### 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}/.json\` + + For documentation and examples, visit: + \`https://ui.shadcn.com/docs/components/${config.base}/\` + ` +} + +function buildRtlSection(config: DesignSystemConfig) { + const template = + config.template === "next-monorepo" ? "next" : (config.template ?? "next") + + return dedent` + ## RTL Support + + Add \`dir="rtl"\` to your root \`\` element: + + \`\`\`html + + \`\`\` + + For full RTL setup including the \`DirectionProvider\`, see the [RTL documentation](https://ui.shadcn.com/docs/rtl/${template}). + ` +} diff --git a/apps/v4/app/(create)/init/md/route.ts b/apps/v4/app/(create)/init/md/route.ts new file mode 100644 index 0000000000..8382fb3af4 --- /dev/null +++ b/apps/v4/app/(create)/init/md/route.ts @@ -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 } + ) + } +} diff --git a/apps/v4/app/(create)/init/parse-config.ts b/apps/v4/app/(create)/init/parse-config.ts new file mode 100644 index 0000000000..173c613d0d --- /dev/null +++ b/apps/v4/app/(create)/init/parse-config.ts @@ -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 + 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 } +} diff --git a/apps/v4/app/(create)/init/route.ts b/apps/v4/app/(create)/init/route.ts index ea941b95f7..916c60c66c 100644 --- a/apps/v4/app/(create)/init/route.ts +++ b/apps/v4/app/(create)/init/route.ts @@ -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 - 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) diff --git a/apps/v4/app/(create)/lib/fonts.ts b/apps/v4/app/(create)/lib/fonts.ts index d13758151f..92bb3f8f2b 100644 --- a/apps/v4/app/(create)/lib/fonts.ts +++ b/apps/v4/app/(create)/lib/fonts.ts @@ -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", diff --git a/apps/v4/lib/components.ts b/apps/v4/lib/components.ts new file mode 100644 index 0000000000..7f87ddf2b5 --- /dev/null +++ b/apps/v4/lib/components.ts @@ -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] diff --git a/apps/v4/next.config.mjs b/apps/v4/next.config.mjs index c02c2f798b..85d97cd94e 100644 --- a/apps/v4/next.config.mjs +++ b/apps/v4/next.config.mjs @@ -121,6 +121,10 @@ const nextConfig = { source: "/docs/:path*.md", destination: "/llm/:path*", }, + { + source: "/init.md", + destination: "/init/md", + }, ] }, } diff --git a/apps/v4/package.json b/apps/v4/package.json index 5095c8d79f..5a46fc0db5 100644 --- a/apps/v4/package.json +++ b/apps/v4/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bab4698ebb..a018241d2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: