diff --git a/.changeset/shaggy-cats-smile.md b/.changeset/shaggy-cats-smile.md new file mode 100644 index 0000000..1867172 --- /dev/null +++ b/.changeset/shaggy-cats-smile.md @@ -0,0 +1,5 @@ +--- +'@openai/create-sites': minor +--- + +Add a shadcn/ui add-on with the complete component set. diff --git a/packages/create-sites/README.md b/packages/create-sites/README.md index 9d29e67..adbe54f 100644 --- a/packages/create-sites/README.md +++ b/packages/create-sites/README.md @@ -5,18 +5,24 @@ Create ChatGPT Sites projects. ```bash npm create @openai/sites@latest my-site -npm create @openai/sites@latest my-site -- --yes --add-ons d1,r2 +npm create @openai/sites@latest my-site -- --yes --add-ons d1,r2,shadcn -pnpm create @openai/sites@latest my-site --yes --add-ons d1,r2 +pnpm create @openai/sites@latest my-site --yes --add-ons d1,r2,shadcn ``` Use `--yes` for noninteractive generation. Dependencies are not installed by default; pass `--install` to install them. The `d1` add-on adds a D1 binding and -Drizzle, `r2` adds an R2 binding, and `auth` adds ChatGPT authentication helpers. +Drizzle, `r2` adds an R2 binding, `auth` adds ChatGPT authentication helpers, +and `shadcn` adds shadcn/ui with its complete component set. + +The `shadcn` add-on uses the Base UI Nova preset with neutral colors, Geist, +Lucide icons, and CSS variables. It includes all 61 entries available through +`shadcn add --all`: 60 component source files plus the fileless `form` +compatibility entry. ```bash npx --yes @openai/create-sites@latest . \ - --yes --add-ons d1,r2 + --yes --add-ons d1,r2,shadcn ``` The generated `.openai/hosting.json` is the source of truth for Sites bindings. diff --git a/packages/create-sites/src/index.ts b/packages/create-sites/src/index.ts index 0f61862..de4f283 100644 --- a/packages/create-sites/src/index.ts +++ b/packages/create-sites/src/index.ts @@ -14,13 +14,17 @@ import { cancel, group, multiselect, text } from '@clack/prompts'; import { Command, InvalidArgumentError, Option } from 'commander'; import { xSync } from 'tinyexec'; -type AddOn = 'd1' | 'r2' | 'auth'; +type AddOn = 'd1' | 'r2' | 'auth' | 'shadcn'; type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'; const addOns: { name: AddOn; description: string }[] = [ { name: 'd1', description: 'Add a Cloudflare D1 database and Drizzle.' }, { name: 'r2', description: 'Add a Cloudflare R2 object storage binding.' }, { name: 'auth', description: 'Add ChatGPT authentication helpers.' }, + { + name: 'shadcn', + description: 'Add shadcn/ui with its complete component set.', + }, ]; const packageManagers: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun']; const allowedExistingEntries = new Set(['.git', '.DS_Store']); @@ -171,6 +175,27 @@ async function createSite( }; } + if (selected.includes('shadcn')) { + manifest.dependencies = { + ...manifest.dependencies, + '@base-ui/react': '1.7.0', + '@shadcn/react': '0.3.0', + 'class-variance-authority': '0.7.1', + clsx: '2.1.1', + cmdk: '1.1.1', + 'date-fns': '4.1.0', + 'embla-carousel-react': '8.5.2', + 'input-otp': '1.4.2', + 'lucide-react': '1.31.0', + 'react-day-picker': '9.8.1', + 'react-resizable-panels': '4.5.8', + recharts: '3.8.0', + shadcn: '4.18.0', + 'tailwind-merge': '3.6.0', + 'tw-animate-css': '1.4.0', + }; + } + await mkdir(destination, { recursive: true }); await cp(base, destination, { recursive: true, force: false }); await rename( @@ -181,7 +206,9 @@ async function createSite( for (const addOn of selected) { await cp(join(templates, 'addons', addOn), destination, { recursive: true, - force: false, + // The shadcn add-on intentionally replaces the base global stylesheet + // with its theme variables and Tailwind imports. + force: addOn === 'shadcn', }); } diff --git a/packages/create-sites/templates/addons/shadcn/app/globals.css b/packages/create-sites/templates/addons/shadcn/app/globals.css new file mode 100644 index 0000000..7fafef8 --- /dev/null +++ b/packages/create-sites/templates/addons/shadcn/app/globals.css @@ -0,0 +1,134 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; +@import 'shadcn/tailwind.css'; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --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); +} + +body { + font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif; +} + +: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); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --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.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 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.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --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(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/packages/create-sites/templates/addons/shadcn/components.json b/packages/create-sites/templates/addons/shadcn/components.json new file mode 100644 index 0000000..f382eb7 --- /dev/null +++ b/packages/create-sites/templates/addons/shadcn/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx new file mode 100644 index 0000000..9daf7bf --- /dev/null +++ b/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx @@ -0,0 +1,78 @@ +import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'; + +import { cn } from '@/lib/utils'; +import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ); +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
+ {children} +
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..46d9eb7 --- /dev/null +++ b/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx @@ -0,0 +1,187 @@ +'use client'; + +import * as React from 'react'; +import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return ; +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return ( + + ); +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ); +} + +function AlertDialogContent({ + className, + size = 'default', + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: 'default' | 'sm'; +}) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CarouselNext({ + className, + variant = 'outline', + size = 'icon-sm', + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, + useCarousel, +}; diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/chart.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/chart.tsx new file mode 100644 index 0000000..0680cf0 --- /dev/null +++ b/packages/create-sites/templates/addons/shadcn/components/ui/chart.tsx @@ -0,0 +1,373 @@ +'use client'; + +import * as React from 'react'; +import * as RechartsPrimitive from 'recharts'; +import type { TooltipValueType } from 'recharts'; + +import { cn } from '@/lib/utils'; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: '', dark: '.dark' } as const; + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const; +type TooltipNameType = number | string; + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = React.createContext(null); + +function useChart() { + const context = React.useContext(ChartContext); + + if (!context) { + throw new Error('useChart must be used within a '); + } + + return context; +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<'div'> & { + config: ChartConfig; + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >['children']; + initialDimension?: { + width: number; + height: number; + }; +}) { + const uniqueId = React.useId(); + const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color, + ); + + if (!colorConfig.length) { + return null; + } + + return ( +