Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/shaggy-cats-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openai/create-sites': minor
---

Add a shadcn/ui add-on with the complete component set.
14 changes: 10 additions & 4 deletions packages/create-sites/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 29 additions & 2 deletions packages/create-sites/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down Expand Up @@ -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(
Expand All @@ -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',
});
}

Expand Down
134 changes: 134 additions & 0 deletions packages/create-sites/templates/addons/shadcn/app/globals.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
25 changes: 25 additions & 0 deletions packages/create-sites/templates/addons/shadcn/components.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
Original file line number Diff line number Diff line change
@@ -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 (
<AccordionPrimitive.Root
data-slot="accordion"
className={cn('flex w-full flex-col', className)}
{...props}
/>
);
}

function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn('not-last:border-b', className)}
{...props}
/>
);
}

function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-lg py-2.5 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none aria-disabled:pointer-events-none aria-disabled:opacity-50',
className,
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}

function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden"
{...props}
>
<div
className={cn(
'pt-0 pb-2.5 h-(--accordion-panel-height) data-ending-style:h-0 data-starting-style:h-0 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4',
className,
)}
>
{children}
</div>
</AccordionPrimitive.Panel>
);
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
Loading