"use client" import * as React from "react" import { Copy01Icon, Globe02Icon, HandPointingRight04Icon, Tick02Icon, } from "@hugeicons/core-free-icons" import { HugeiconsIcon } from "@hugeicons/react" import { cn } from "cn" import { useConfig } from "@/hooks/use-config" import { copyToClipboardWithMeta } from "@/components/copy-button" import { BASES, buildThemeForPreset, DEFAULT_CONFIG, type BaseName, type DesignSystemConfig, } from "@/registry/config" import { Button } from "@/styles/base-nova/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTrigger, } from "@/styles/base-nova/ui/dialog" import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet, FieldTitle, } from "@/styles/base-nova/ui/field" import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group" import { Switch } from "@/styles/base-nova/ui/switch" import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/styles/base-nova/ui/tabs" import { ToggleGroup, ToggleGroupItem, } from "@/styles/base-nova/ui/toggle-group" import { usePresetCode } from "@/app/(app)/(create)/hooks/use-design-system" import { useDesignSystemSearchParams, type DesignSystemSearchParams, } from "@/app/(app)/(create)/lib/search-params" import { getFramework, getTemplateValue, NO_MONOREPO_FRAMEWORKS, TEMPLATES, } from "@/app/(app)/(create)/lib/templates" const TURBOREPO_LOGO = 'Turborepo' const ORIGIN = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000" const IS_LOCAL_DEV = ORIGIN.includes("localhost") const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest" const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const type PackageManager = (typeof PACKAGE_MANAGERS)[number] const APPLY_MODES = [ { value: "full", title: "Full preset", description: "Everything from the preset, including components, theme, and fonts.", flag: null, label: "full preset", }, { value: "theme", title: "Theme only", description: "Theme tokens only, like colors, radii, and shadows. Components stay as they are.", flag: "--only theme", label: "--only theme", }, { value: "font", title: "Fonts only", description: "Only preset fonts for body and headings. Components stay as they are.", flag: "--only font", label: "--only font", }, ] as const type ApplyMode = (typeof APPLY_MODES)[number]["value"] type ProjectFormTab = "new-project" | "existing-project" | "theme" type CopyTarget = "command" | "apply" | "theme" type ThemeCssVars = NonNullable< ReturnType["cssVars"] > function formatCssVarsRule(selector: string, cssVars?: Record) { const declarations = Object.entries(cssVars ?? {}) .map(([key, value]) => ` --${key}: ${value};`) .join("\n") return `${selector} {\n${declarations}\n}` } function formatThemeCss(cssVars: ThemeCssVars) { return [ formatCssVarsRule(":root", cssVars.light), formatCssVarsRule(".dark", cssVars.dark), ].join("\n\n") } export function ProjectForm({ className, }: React.ComponentProps) { const [open, setOpen] = React.useState(false) const [params, setParams] = useDesignSystemSearchParams() const presetCode = usePresetCode() const [config, setConfig] = useConfig() const [copiedTarget, setCopiedTarget] = React.useState( null ) const [applyMode, setApplyMode] = React.useState("full") const [activeTab, setActiveTab] = React.useState("new-project") const packageManager = (config.packageManager || "pnpm") as PackageManager const framework = React.useMemo( () => getFramework(params.template ?? "next"), [params.template] ) const isMonorepo = React.useMemo( () => params.template?.endsWith("-monorepo") ?? false, [params.template] ) const hasMonorepo = !NO_MONOREPO_FRAMEWORKS.includes( framework as (typeof NO_MONOREPO_FRAMEWORKS)[number] ) const commands = React.useMemo(() => { const presetFlag = ` --preset ${presetCode}` const baseFlag = params.base !== DEFAULT_CONFIG.base ? ` --base ${params.base}` : "" const templateFlag = ` --template ${framework}` const monorepoFlag = isMonorepo ? " --monorepo" : "" const rtlFlag = params.rtl ? " --rtl" : "" const pointerFlag = params.pointer ? " --pointer" : "" const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}${pointerFlag}` return IS_LOCAL_DEV ? { pnpm: `shadcn init${flags}`, npm: `shadcn init${flags}`, yarn: `shadcn init${flags}`, bun: `shadcn init${flags}`, } : { pnpm: `pnpm dlx shadcn${SHADCN_VERSION} init${flags}`, npm: `npx shadcn${SHADCN_VERSION} init${flags}`, yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`, bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`, } }, [ framework, isMonorepo, params.base, params.pointer, params.rtl, presetCode, ]) const command = commands[packageManager] const applyCommands = React.useMemo(() => { const presetFlag = ` --preset ${presetCode}` const onlyFlag = applyMode === "theme" ? " --only theme" : applyMode === "font" ? " --only font" : "" const flags = `${presetFlag}${onlyFlag}` return IS_LOCAL_DEV ? { pnpm: `shadcn apply${flags}`, npm: `shadcn apply${flags}`, yarn: `shadcn apply${flags}`, bun: `shadcn apply${flags}`, } : { pnpm: `pnpm dlx shadcn${SHADCN_VERSION} apply${flags}`, npm: `npx shadcn${SHADCN_VERSION} apply${flags}`, yarn: `yarn dlx shadcn${SHADCN_VERSION} apply${flags}`, bun: `bunx --bun shadcn${SHADCN_VERSION} apply${flags}`, } }, [applyMode, presetCode]) const applyCommand = applyCommands[packageManager] const themeConfig = React.useMemo(() => { const isRadiusLocked = params.style === "lyra" || params.style === "sera" return { ...DEFAULT_CONFIG, base: params.base, style: params.style, baseColor: params.baseColor, theme: params.theme, chartColor: params.chartColor, iconLibrary: params.iconLibrary, font: params.font, fontHeading: params.fontHeading, menuAccent: params.menuAccent, menuColor: params.menuColor, radius: isRadiusLocked ? "none" : params.radius, template: params.template, rtl: params.rtl, pointer: params.pointer, } }, [ params.base, params.baseColor, params.chartColor, params.font, params.fontHeading, params.iconLibrary, params.menuAccent, params.menuColor, params.pointer, params.radius, params.rtl, params.style, params.template, params.theme, ]) const themeCss = React.useMemo(() => { const theme = buildThemeForPreset(themeConfig) if (!theme.cssVars) { return "" } return formatThemeCss(theme.cssVars) }, [themeConfig]) React.useEffect(() => { if (copiedTarget) { const timer = setTimeout(() => setCopiedTarget(null), 2000) return () => clearTimeout(timer) } }, [copiedTarget]) const handleCopy = React.useCallback(() => { const properties: Record = { command, } if (params.template) { properties.template = params.template } copyToClipboardWithMeta(command, { name: "copy_npm_command", properties, }) setCopiedTarget("command") }, [command, params.template]) const handleCopyApply = React.useCallback(() => { copyToClipboardWithMeta(applyCommand, { name: "copy_apply_command", properties: { command: applyCommand, applyMode, }, }) setCopiedTarget("apply") }, [applyCommand, applyMode]) const handleCopyTheme = React.useCallback(() => { copyToClipboardWithMeta(themeCss, { name: "copy_theme_code", properties: { preset: presetCode, baseColor: params.baseColor, theme: params.theme, format: "css", }, }) setCopiedTarget("theme") }, [params.baseColor, params.theme, presetCode, themeCss]) return ( }> Get Code
setActiveTab((values[0] as typeof activeTab) ?? "new-project") } aria-label="Project type" spacing={2} className="**:data-[slot=toggle-group-item]:data-pressed:bg-neutral-700/70" > New Project Existing Project Theme {activeTab === "new-project" && (
Template Base
Options Use pointer on buttons setParams({ pointer: checked === true }) } /> Create a monorepo { const framework = getFramework( params.template ?? "next" ) setParams({ template: getTemplateValue( framework, checked === true ) as typeof params.template, }) }} /> Enable RTL support setParams({ rtl: checked === true }) } />
{ setConfig((prev) => ({ ...prev, packageManager: value as PackageManager, })) }} className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border" >
{PACKAGE_MANAGERS.map((manager) => { return ( {manager} ) })}
{Object.entries(commands).map(([key, cmd]) => { return (
{cmd}
) })}
)} {activeTab === "existing-project" && (
Apply Preset Pick which parts of the preset to apply.
{ setConfig((prev) => ({ ...prev, packageManager: value as PackageManager, })) }} className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border" >
{PACKAGE_MANAGERS.map((manager) => { return ( {manager} ) })}
{Object.entries(applyCommands).map(([key, cmd]) => { return (
{cmd}
) })}
)} {activeTab === "theme" && (
Theme Tokens Copy the CSS variables for this preset.
globals.css
                        {themeCss}
                      
)}
) } const TemplateGrid = React.memo(function TemplateGrid({ template, setParams, }: { template: DesignSystemSearchParams["template"] setParams: ReturnType[1] }) { const isMonorepo = template?.endsWith("-monorepo") ?? false const framework = getFramework(template ?? "next") const handleTemplateChange = React.useCallback( (value: string) => { setParams({ template: getTemplateValue( value, isMonorepo ) as DesignSystemSearchParams["template"], }) }, [isMonorepo, setParams] ) return ( {TEMPLATES.map((item) => (
{item.title}
))}
) }) const ApplyModeGrid = React.memo(function ApplyModeGrid({ mode, setMode, }: { mode: ApplyMode setMode: (mode: ApplyMode) => void }) { return ( setMode(value as ApplyMode)} aria-label="Apply" > {APPLY_MODES.map((option) => ( {option.title} {option.description} ))} ) }) const BaseGrid = React.memo(function BaseGrid({ base, setParams, }: { base: DesignSystemSearchParams["base"] setParams: ReturnType[1] }) { const handleBaseChange = React.useCallback( (value: string) => { setParams({ base: value as BaseName }) }, [setParams] ) return ( {BASES.map((item) => (
{item.title} ))} ) })