Compare commits

...

31 Commits

Author SHA1 Message Date
github-actions[bot]
061083006f chore(release): version packages (#4857)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-15 13:40:31 +04:00
Jack Herrington
1af66c2d08 Adding support for ~ in target specification (#4848)
* Adding support for ~ in target specification

* test(shadcn): add a test for srcDir false

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2024-09-15 13:27:48 +04:00
shadcn
0993d98cc7 ci: update stale config (#4858) 2024-09-15 13:25:06 +04:00
Hichem Fantar
52d223393a (fix Input) choose file text color in dark mode (#4843)
This pull request fixes the issue with the choose file text color in dark mode. The changes ensure that the text color is correctly displayed in dark mode.

the reason this bug doesn't happen in the shadcn website is because it sets `color-scheme:dark` on the document;

this fix makes sure this always works even if color scheme isn't set.

Closes #4842

![image](https://github.com/user-attachments/assets/37cb3f2a-c1a7-49b3-8ef1-77823dd7bd45)

![image](https://github.com/user-attachments/assets/92913fec-5651-4cfc-8e16-2f12c06c33b8)
2024-09-15 09:20:19 +00:00
Shivang Rathore
207b69fe8d fix(cli): Ensure .scss files are considered in getTailwindCssFile (#4797)
* fix(cli): Ensure .scss files are considered in getTailwindCssFile

- Updated getTailwindCssFile function to include .scss files when checking for Tailwind CSS configuration.
- This change ensures that .scss files are properly identified and handled, improving compatibility with projects using SCSS.

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2024-09-15 12:55:08 +04:00
Rana Haris Ali
408760a93b fix: correct grammar in "path does not contain a package.json" message (#4815)
* fix: correct grammar in "path does not contain a package.json" message

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2024-09-15 12:51:23 +04:00
Devansh Mahant
a9ab7afebf Fix Incorrect Hook Import Path in Toast Component Example in ShadCN Docs (#4811)
#### Summary:
This pull request addresses a documentation error found in the ShadCN website's Toast component example. Specifically, the import route for the hook is incorrect in the example. and fixes [#4816](https://github.com/shadcn-ui/ui/issues/4816)

#### Issue:
Upon reviewing the [ShadCN Toast documentation](https://ui.shadcn.com/docs/components/toast), I found that the import path for the Toast component hook was wrongly mentioned as being located in the `components` folder. According to the `component.json` file, the correct location of the hook is within the `hooks` folder inside the main directory.

#### Fix:
- Corrected the import path in the Toast component example from `components` to `hooks`, as per the `component.json` file structure.

#### Example of Fix:
**Before:**
```js
import { useToast } from "@/components/use-toast";
```

**After:**
```js
import { useToast } from "@/hooks/use-toast";
```

#### Testing:
- Verified that the corrected path resolves correctly.
- Ensured the example works as expected after the change.

#### Impact:
This fix prevents confusion for users following the example and ensures that the import path accurately reflects the project structure, improving the overall developer experience.
2024-09-15 08:45:29 +00:00
Quinn Blenkinsop
b6221ea524 fix(www): update broken link to headless ui (#3542)
The link appears to have broken at some point, I've updated it

Co-authored-by: shadcn <m@shadcn.com>
2024-09-15 12:31:28 +04:00
github-actions[bot]
9ef7967b0d chore(release): version packages (#4821)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-12 17:54:41 +04:00
shadcn
64b2f1a5ad feat(shadcn): add experimental docs (#4820)
* feat(shadcn): add cli docs

* chore: add changeset

* fix: tests
2024-09-12 17:51:59 +04:00
github-actions[bot]
f4ca57a79c chore(release): version packages (#4788)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-09 12:34:19 +04:00
shadcn
99ff9caf71 fix(shadcn): add src to content in tailwind config (#4787)
* feat(shadcn): handle src dir

* chore: changeset
2024-09-09 12:32:12 +04:00
github-actions[bot]
cd9a55b76a chore(release): version packages (#4777)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-07 23:42:29 +04:00
shadcn
49373eed96 fix(shadcn): better error handling (#4776)
* fix(shadcn): better error messages

* chore: changeset
2024-09-07 23:35:37 +04:00
shadcn
078dfe6607 docs(www): Open in v0 docs (#4734)
* feat(www): open in v0

* feat: update copy

* fix: sidebar link

* fix(tests): snapshots
2024-09-04 00:37:57 +04:00
xuxucode
77fc5ec8db Fix use-toast module path (#4728) 2024-09-03 18:37:11 +00:00
github-actions[bot]
cfba3fdf70 chore(release): version packages (#4730)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-03 22:22:26 +04:00
adrianhelvikspond
4e4118f3cf Fix init command for default style - fixes #4722 (#4724)
* Fix init command for default style

The empty dependency string was tripping up package managers.

* fix(www): registry dependencies for default

---------

Co-authored-by: shadcn <m@shadcn.com>
2024-09-03 22:21:51 +04:00
shadcn
faa7a67fb3 fix(shadcn): init with src (#4731)
* fix(shadcn): init with src

* chore: add changesets
2024-09-03 22:16:59 +04:00
shadcn
701e1160ea feat(shadcn): add support for src dir (#4729)
* feat(shadcn): add support for src dir

* chore: add changesets
2024-09-03 21:47:15 +04:00
shadcn
f5931f8d09 docs(www): update installation docs (#4725) 2024-09-03 16:41:48 +04:00
shadcn
5a28937c6e Merge branch 'main' of github.com:shadcn/ui 2024-09-03 16:35:51 +04:00
shadcn
0b74059d38 docs(www): update laravel docs 2024-09-03 16:35:36 +04:00
shadcn
fab9877586 fix(shadcn): handle quote in theme values (#4726)
* fix(shadcn): handle quote in theme values

* chore(shadcn): fix theme values bug
2024-09-03 16:22:45 +04:00
shadcn
0f7591f67c docs(www): updates docs for Astro (#4723) 2024-09-03 15:35:24 +04:00
shadcn
81c7e44863 fix(www): url 2024-08-31 03:15:10 +04:00
shadcn
2fac3e40c2 fix(www): hide sidebar-01 for now 2024-08-31 03:10:14 +04:00
shadcn
5ad11ff851 docs(www): update callout 2024-08-31 03:01:42 +04:00
shadcn
6b92dd8eaf Merge branch 'main' of github.com:shadcn/ui 2024-08-31 02:57:49 +04:00
shadcn
84540f551d fix: blocks 2024-08-31 02:57:37 +04:00
shadcn
99588fff8f fix: cli (#4669) 2024-08-31 02:09:14 +04:00
50 changed files with 879 additions and 275 deletions

View File

@@ -16,11 +16,12 @@ jobs:
name: "Close stale issues with no reproduction"
with:
repo-token: ${{ secrets.STALE_TOKEN }}
close-issue-message: "This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you."
close-issue-message: "This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please reopen or leave a comment. Thank you.\n(This is an automated message.)"
days-before-issue-close: 7
days-before-issue-stale: 15
days-before-issue-stale: 30
stale-pr-label: "stale?"
days-before-pr-close: -1
days-before-pr-stale: -1
days-before-pr-close: 7
days-before-pr-stale: 15
only-pr-labels: "postpone: more info or changes requested,please add a reproduction"
exempt-issue-labels: "roadmap,next,bug"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close

View File

@@ -0,0 +1,13 @@
import { LoginForm } from "@/registry/default/block/login-01/components/login-form"
export const iframeHeight = "870px"
export const containerClassName = "w-full h-full"
export default function Page() {
return (
<div className="flex h-screen w-full items-center justify-center px-4">
<LoginForm />
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { AppSidebar } from "@/registry/default/block/sidebar-01/components/app-sidebar"
import {
SidebarLayout,
SidebarTrigger,
} from "@/registry/default/block/sidebar-01/ui/sidebar"
export const iframeHeight = "870px"
export const containerClassName = "w-full h-full"
export default async function Page() {
const { cookies } = await import("next/headers")
return (
<SidebarLayout
defaultOpen={cookies().get("sidebar:state")?.value === "true"}
>
<AppSidebar />
<main className="flex flex-1 flex-col p-2 transition-all duration-300 ease-in-out">
<div className="h-full rounded-md border-2 border-dashed p-2">
<SidebarTrigger />
</div>
</main>
</SidebarLayout>
)
}

View File

@@ -2145,7 +2145,7 @@ export const Index: Record<string, any> = {
registryDependencies: ["avatar","button","collapsible","dropdown-menu","drawer","separator","input","popover","sheet","progress","card","use-mobile"],
files: ["registry/new-york/block/sidebar-01/page.tsx","registry/new-york/block/sidebar-01/components/app-sidebar.tsx","registry/new-york/block/sidebar-01/components/nav-main.tsx","registry/new-york/block/sidebar-01/components/nav-projects.tsx","registry/new-york/block/sidebar-01/components/nav-secondary.tsx","registry/new-york/block/sidebar-01/components/nav-user.tsx","registry/new-york/block/sidebar-01/components/storage-card.tsx","registry/new-york/block/sidebar-01/components/team-switcher.tsx","registry/new-york/block/sidebar-01/hooks/use-sidebar.tsx","registry/new-york/block/sidebar-01/ui/sidebar.tsx"],
component: React.lazy(() => import("@/registry/new-york/block/sidebar-01/page.tsx")),
source: "__registry__/new-york/block/sidebar-01.tsx",
source: "__registry__/new-york/block/sidebar-01/page.tsx",
category: "Application",
subcategory: "Dashboard",
chunks: []
@@ -2156,7 +2156,7 @@ export const Index: Record<string, any> = {
registryDependencies: ["button","card","input","label"],
files: ["registry/new-york/block/login-01/page.tsx","registry/new-york/block/login-01/components/login-form.tsx"],
component: React.lazy(() => import("@/registry/new-york/block/login-01/page.tsx")),
source: "__registry__/new-york/block/login-01.tsx",
source: "__registry__/new-york/block/login-01/page.tsx",
category: "Authentication",
subcategory: "Login",
chunks: []
@@ -5501,7 +5501,7 @@ export const Index: Record<string, any> = {
registryDependencies: ["avatar","button","collapsible","dropdown-menu","drawer","separator","input","popover","sheet","progress","card","use-mobile"],
files: ["registry/default/block/sidebar-01/page.tsx","registry/default/block/sidebar-01/components/app-sidebar.tsx","registry/default/block/sidebar-01/components/nav-main.tsx","registry/default/block/sidebar-01/components/nav-projects.tsx","registry/default/block/sidebar-01/components/nav-secondary.tsx","registry/default/block/sidebar-01/components/nav-user.tsx","registry/default/block/sidebar-01/components/storage-card.tsx","registry/default/block/sidebar-01/components/team-switcher.tsx","registry/default/block/sidebar-01/hooks/use-sidebar.tsx","registry/default/block/sidebar-01/ui/sidebar.tsx"],
component: React.lazy(() => import("@/registry/default/block/sidebar-01/page.tsx")),
source: "__registry__/default/block/sidebar-01.tsx",
source: "__registry__/default/block/sidebar-01/page.tsx",
category: "Application",
subcategory: "Dashboard",
chunks: []
@@ -5512,7 +5512,7 @@ export const Index: Record<string, any> = {
registryDependencies: ["button","card","input","label"],
files: ["registry/default/block/login-01/page.tsx","registry/default/block/login-01/components/login-form.tsx"],
component: React.lazy(() => import("@/registry/default/block/login-01/page.tsx")),
source: "__registry__/default/block/login-01.tsx",
source: "__registry__/default/block/login-01/page.tsx",
category: "Authentication",
subcategory: "Login",
chunks: []

View File

@@ -0,0 +1,13 @@
import { LoginForm } from "@/registry/new-york/block/login-01/components/login-form"
export const iframeHeight = "870px"
export const containerClassName = "w-full h-full"
export default function Page() {
return (
<div className="flex h-screen w-full items-center justify-center px-4">
<LoginForm />
</div>
)
}

View File

@@ -0,0 +1,25 @@
import { AppSidebar } from "@/registry/new-york/block/sidebar-01/components/app-sidebar"
import {
SidebarLayout,
SidebarTrigger,
} from "@/registry/new-york/block/sidebar-01/ui/sidebar"
export const iframeHeight = "870px"
export const containerClassName = "w-full h-full"
export default async function Page() {
const { cookies } = await import("next/headers")
return (
<SidebarLayout
defaultOpen={cookies().get("sidebar:state")?.value === "true"}
>
<AppSidebar />
<main className="flex flex-1 flex-col p-2 transition-all duration-300 ease-in-out">
<div className="h-full rounded-md border-2 border-dashed p-2">
<SidebarTrigger />
</div>
</main>
</SidebarLayout>
)
}

View File

@@ -5,7 +5,10 @@ import { ThemesSwitcher } from "@/components/themes-selector"
export default async function BlocksPage() {
const blocks = (await getAllBlockIds()).filter(
(name) => !name.startsWith("chart-")
(name) =>
!name.startsWith("chart-") &&
!name.startsWith("sidebar-01") &&
!name.startsWith("login-01")
)
// These themes are not compatible with the blocks yet.

View File

@@ -11,6 +11,7 @@ import { siteConfig } from "@/config/site"
import { getTableOfContents } from "@/lib/toc"
import { absoluteUrl, cn } from "@/lib/utils"
import { Mdx } from "@/components/mdx-components"
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
import { DocsPager } from "@/components/pager"
import { DashboardTableOfContents } from "@/components/toc"
import { badgeVariants } from "@/registry/new-york/ui/badge"
@@ -135,17 +136,14 @@ export default async function DocPage({ params }: DocPageProps) {
</div>
<DocsPager doc={doc} />
</div>
{doc.toc && (
<div className="hidden text-sm xl:block">
<div className="sticky top-16 -mt-10 pt-4">
<ScrollArea className="pb-10">
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] py-12">
<DashboardTableOfContents toc={toc} />
</div>
</ScrollArea>
</div>
<div className="hidden text-sm xl:block">
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] pt-4">
<ScrollArea className="h-full pb-10">
{doc.toc && <DashboardTableOfContents toc={toc} />}
<OpenInV0Cta className="mt-6 max-w-[80%]" />
</ScrollArea>
</div>
)}
</div>
</main>
)
}

View File

@@ -7,7 +7,7 @@ import { Separator } from "@/registry/new-york/ui/separator"
export function Announcement() {
return (
<Link
href="/docs/components/chart"
href="/docs/changelog"
className="group inline-flex items-center px-0.5 text-sm font-medium"
>
<PieChart className="h-4 w-4" />{" "}

View File

@@ -0,0 +1,35 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york/ui/button"
export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
return (
<div
className={cn(
"group relative flex flex-col gap-2 rounded-lg border p-4 text-sm",
className
)}
>
<div className="text-balance text-lg font-semibold leading-tight group-hover:underline">
Bring your app built with shadcn to life on Vercel
</div>
<div>Trusted by OpenAI, Sonos, Chick-fil-A, and more.</div>
<div>
Vercel provides tools and infrastructure to deploy apps and features at
scale.
</div>
<Button size="sm" className="mt-2 w-fit">
Deploy Now
</Button>
<Link
href="https://vercel.com/new?utm_source=shadcn_site&utm_medium=web&utm_campaign=docs_cta_deploy_now_callout"
target="_blank"
rel="noreferrer"
className="absolute inset-0"
>
<span className="sr-only">Deploy to Vercel</span>
</Link>
</div>
)
}

View File

@@ -26,7 +26,7 @@ export function DashboardTableOfContents({ toc }: TocProps) {
const activeHeading = useActiveItem(itemIds)
const mounted = useMounted()
if (!toc?.items || !mounted) {
if (!toc?.items?.length) {
return null
}

View File

@@ -77,6 +77,12 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/typography",
items: [],
},
{
title: "Open in v0",
href: "/docs/v0",
items: [],
label: "New",
},
{
title: "Figma",
href: "/docs/figma",
@@ -89,6 +95,46 @@ export const docsConfig: DocsConfig = {
},
],
},
{
title: "Installation",
items: [
{
title: "Next.js",
href: "/docs/installation/next",
items: [],
},
{
title: "Vite",
href: "/docs/installation/vite",
items: [],
},
{
title: "Remix",
href: "/docs/installation/remix",
items: [],
},
{
title: "Astro",
href: "/docs/installation/astro",
items: [],
},
{
title: "Laravel",
href: "/docs/installation/laravel",
items: [],
},
{
title: "Gatsby",
href: "/docs/installation/gatsby",
items: [],
},
{
title: "Manual",
href: "/docs/installation/manual",
items: [],
},
],
},
{
title: "Components",
items: [

View File

@@ -5,7 +5,7 @@ description: Use the CLI to add components to your project.
<Callout>
**Note:** We just released a new `shadcn` CLI to make it easier to add components to your project. See the [changelog](/docs/changelog) for more information.
**Note:** We just released a new `shadcn` CLI. See the [changelog](/docs/changelog) for more information.
</Callout>

View File

@@ -12,7 +12,7 @@ links:
Every data table or datagrid I've created has been unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/guide/introduction#what-is-headless-ui) provides.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that [headless UI](https://tanstack.com/table/v8/docs/introduction#what-is-headless-ui) provides.
So instead of a data-table component, I thought it would be more helpful to provide a guide on how to build your own.

View File

@@ -104,7 +104,7 @@ export default function RootLayout({ children }) {
The `useToast` hook returns a `toast` function that you can use to display a toast.
```tsx
import { useToast } from "@/components/ui/use-toast"
import { useToast } from "@/hooks/use-toast"
```
```tsx {2,7-10}

View File

@@ -18,18 +18,12 @@ npm create astro@latest
You will be asked a few questions to configure your project:
```txt showLineNumbers
- Where should we create your new project?
./your-app-name
- How would you like to start your new project?
Choose a starter template (or Empty)
- Install dependencies?
Yes
- Do you plan to write TypeScript?
Yes
- How strict should TypeScript be?
Strict
- Initialize a new git repository? (optional)
Yes/No
- Where should we create your new project? ./your-app-name
- How would you like to start your new project? Choose a template
- Do you plan to write TypeScript? Yes
- How strict should TypeScript be? Strict
- Install dependencies? Yes
- Initialize a new git repository? (optional) Yes/No
```
### Add React to your project
@@ -48,23 +42,47 @@ Answer `Yes` to all the question prompted by the CLI when installing React.
### Add Tailwind CSS to your project
Install Tailwind CSS using the Astro CLI:
```bash
npx astro add tailwind
```
<Callout className="mt-4">
<Step>Create a `styles/globals.css` file in the `src` folder.</Step>
Answer `Yes` to all the question prompted by the CLI when installing Tailwind CSS.
```css title="styles/globals.css" showLineNumbers
@tailwind base;
@tailwind components;
@tailwind utilities;
```
</Callout>
<Step>Import the `globals.css` file</Step>
Import the `styles/globals.css` file in the `src/pages/index.astro` file:
```ts title="src/pages/index.astro" showLineNumbers
---
import '@/styles/globals.css'
---
```
<Step>Update `astro.config.mjs` and set `applyBaseStyles` to `false`</Step>
To prevent serving the Tailwind base styles twice, we need to tell Astro not to apply the base styles, since we already include them in our own `globals.css` file. To do this, set the `applyBaseStyles` config option for the tailwind plugin in `astro.config.mjs` to `false`.
```js title="astro.config.mjs" {3-5} showLineNumbers
export default defineConfig({
integrations: [
tailwind({
applyBaseStyles: false,
}),
],
})
```
### Edit tsconfig.json file
Add the following code to the `tsconfig.json` file to resolve paths:
```ts {4-9} showLineNumbers
```ts title="tsconfig.json" {4-9} showLineNumbers
{
"compilerOptions": {
// ...
@@ -81,62 +99,12 @@ Add the following code to the `tsconfig.json` file to resolve paths:
### Run the CLI
Run the `shadcn-ui` init command to setup your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init
```
### Configure components.json
You will be asked a few questions to configure `components.json`:
```txt showLineNumbers
Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? ./src/styles/globals.css
Do you want to use CSS variables for colors? no / yes
Where is your tailwind.config.js located? tailwind.config.mjs
Configure the import alias for components: @/components
Configure the import alias for utils: @/lib/utils
Are you using React Server Components? no
```
### Import the globals.css file
Import the `globals.css` file in the `src/pages/index.astro` file:
```ts {2} showLineNumbers
---
import '@/styles/globals.css'
---
```
### Update astro tailwind config
To prevent serving the Tailwind base styles twice, we need to tell Astro not to apply the base styles, since we already include them in our own `globals.css` file. To do this, set the `applyBaseStyles` config option for the tailwind plugin in `astro.config.mjs` to `false`.
```ts {3-5} showLineNumbers
export default defineConfig({
integrations: [
tailwind({
applyBaseStyles: false,
}),
],
})
```
### Update tailwind.config.mjs
When running `npx shadcn@latest init`, your tailwind config for content will be overwritten. To fix this, change the `module.exports` to `export default` and the `content` section with the code below to your `tailwind.config.mjs` file:
```js {1-4} showLineNumbers
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
}
```
### That's it
You can now start adding components to your project.

View File

@@ -45,19 +45,6 @@ description: How to install dependencies and structure your app.
</svg>
<p className="font-medium mt-2">Remix</p>
</LinkedCard>
<LinkedCard href="/docs/installation/gatsby">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>Gatsby</title>
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0zm0 2.571c3.171 0 5.915 1.543 7.629 3.858l-1.286 1.115C16.886 5.572 14.571 4.286 12 4.286c-3.343 0-6.171 2.143-7.286 5.143l9.857 9.857c2.486-.857 4.373-3 4.973-5.572h-4.115V12h6c0 4.457-3.172 8.228-7.372 9.17L2.83 9.944C3.772 5.743 7.543 2.57 12 2.57zm-9.429 9.6l9.344 9.258c-2.4-.086-4.801-.943-6.601-2.743-1.8-1.8-2.743-4.201-2.743-6.515z" />
</svg>
<p className="font-medium mt-2">Gatsby</p>
</LinkedCard>
<LinkedCard href="/docs/installation/astro">
<svg
role="img"
@@ -86,6 +73,19 @@ description: How to install dependencies and structure your app.
</svg>
<p className="font-medium mt-2">Laravel</p>
</LinkedCard>
<LinkedCard href="/docs/installation/gatsby">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>Gatsby</title>
<path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0zm0 2.571c3.171 0 5.915 1.543 7.629 3.858l-1.286 1.115C16.886 5.572 14.571 4.286 12 4.286c-3.343 0-6.171 2.143-7.286 5.143l9.857 9.857c2.486-.857 4.373-3 4.973-5.572h-4.115V12h6c0 4.457-3.172 8.228-7.372 9.17L2.83 9.944C3.772 5.743 7.543 2.57 12 2.57zm-9.429 9.6l9.344 9.258c-2.4-.086-4.801-.943-6.601-2.743-1.8-1.8-2.743-4.201-2.743-6.515z" />
</svg>
<p className="font-medium mt-2">Gatsby</p>
</LinkedCard>
<LinkedCard href="/docs/installation/manual">
<svg
role="img"

View File

@@ -15,7 +15,7 @@ laravel new my-app --typescript --breeze --stack=react --git --no-interaction
### Run the CLI
Run the `shadcn-ui` init command to setup your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init
@@ -26,106 +26,9 @@ npx shadcn@latest init
You will be asked a few questions to configure `components.json`:
```txt showLineNumbers
Would you like to use TypeScript (recommended)? no / yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? resources/css/app.css
Do you want to use CSS variables for colors? no / yes
Where is your tailwind.config.js located? tailwind.config.js
Configure the import alias for components: @/Components
Configure the import alias for utils: @/lib/utils
Are you using React Server Components? no / yes
```
### Update tailwind.config.js
The `shadcn-ui` CLI will automatically overwrite your `tailwind.config.js`. Update it to look like this:
```js
import forms from "@tailwindcss/forms"
import defaultTheme from "tailwindcss/defaultTheme"
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: [
"./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php",
"./storage/framework/views/*.php",
"./resources/views/**/*.blade.php",
"./resources/js/**/*.tsx",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: ["Figtree", ...defaultTheme.fontFamily.sans],
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [forms, require("tailwindcss-animate")],
}
Which style would you like to use?
Which color would you like to use as base color?
Do you want to use CSS variables for colors? yes
```
### That's it

View File

@@ -13,6 +13,16 @@ Run the `init` command to create a new Next.js project or to setup an existing o
npx shadcn@latest init
```
<Callout className="mt-4">
You can use the `-d` flag for defaults i.e `new-york`, `zinc` and `yes` for the css variables.
```bash
npx shadcn@latest init -d
```
</Callout>
### Configure components.json
You will be asked a few questions to configure `components.json`:
@@ -23,40 +33,6 @@ Which color would you like to use as base color? Zinc
Do you want to use CSS variables for colors? no / yes
```
### App structure
Here's how I structure my Next.js apps. You can use this as a reference:
```txt {6-10,14-15}
.
├── app
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── ui
│ │ ├── alert-dialog.tsx
│ │ ├── button.tsx
│ │ ├── dropdown-menu.tsx
│ │ └── ...
│ ├── main-nav.tsx
│ ├── page-header.tsx
│ └── ...
├── lib
│ └── utils.ts
├── styles
│ └── globals.css
├── next.config.js
├── package.json
├── postcss.config.js
├── tailwind.config.ts
└── tsconfig.json
```
- I place the UI components in the `components/ui` folder.
- The rest of the components such as `<PageHeader />` and `<MainNav />` are placed in the `components` folder.
- The `lib` folder contains all the utility functions. I have a `utils.ts` where I define the `cn` helper.
- The `styles` folder contains the global CSS.
### That's it
You can now start adding components to your project.

View File

@@ -0,0 +1,30 @@
---
title: Open in v0
description: Open components in v0 for customization.
---
Every component on ui.shadcn.com is editable on [v0 by Vercel](https://v0.dev). This allows you to easily customize the components in natural language and paste into your app.
<a href="https://vercel.com/signup?utm_source=shad&utm_medium=web&utm_campaign=docs_cta_signup">
<Image
src="/images/open-in-v0.png"
width="716"
height="420"
alt="Open in v0"
className="border dark:hidden shadow-sm rounded-lg overflow-hidden mt-6 w-full"
/>
<Image
src="/images/open-in-v0-dark.png"
width="716"
height="420"
alt="Open in v0"
className="border hidden dark:block shadow-sm rounded-lg overflow-hidden mt-6 w-full"
/>
<span class="sr-only">Open in v0</span>
</a>
To use v0, sign-up for a free [Vercel account here](https://vercel.com/signup?utm_source=shad&utm_medium=web&utm_campaign=docs_cta_signup). In addition to v0, this gives you free access to Vercel's frontend cloud platform by the creators of Next.js, where you can deploy and host your project for free.
Learn more about getting started with [Vercel here](https://vercel.com/docs/getting-started-with-vercel?utm_source=shadcn_site&utm_medium=web&utm_campaign=docs_cta_about_vercel).
Learn more about getting started with [v0 here](https://v0.dev/faq).

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -4,8 +4,7 @@
"dependencies": [
"tailwindcss-animate",
"class-variance-authority",
"lucide-react",
""
"lucide-react"
],
"registryDependencies": [
"utils"

View File

@@ -4,7 +4,7 @@
"files": [
{
"path": "ui/input.tsx",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"type": "registry:ui",
"target": ""
}

View File

@@ -4,7 +4,7 @@
"files": [
{
"path": "ui/input.tsx",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n ({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n ref={ref}\n {...props}\n />\n )\n }\n)\nInput.displayName = \"Input\"\n\nexport { Input }\n",
"type": "registry:ui",
"target": ""
}

View File

@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View File

@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View File

@@ -62,6 +62,7 @@ export const registryEntrySchema = z.object({
category: z.string().optional(),
subcategory: z.string().optional(),
chunks: z.array(blockChunkSchema).optional(),
docs: z.string().optional(),
})
export const registrySchema = z.array(registryEntrySchema)

View File

@@ -231,6 +231,18 @@ export const Index: Record<string, any> = {
// // Write the source file for blocks only.
sourceFilename = `__registry__/${style.name}/${type}/${item.name}.tsx`
if (item.files) {
const files = item.files.map((file) =>
typeof file === "string"
? { type: "registry:page", path: file }
: file
)
if (files?.length) {
sourceFilename = `__registry__/${style.name}/${files[0].path}`
}
}
const sourcePath = path.join(process.cwd(), sourceFilename)
if (!existsSync(sourcePath)) {
await fs.mkdir(sourcePath, { recursive: true })
@@ -414,16 +426,21 @@ async function buildStylesIndex() {
for (const style of styles) {
const targetPath = path.join(REGISTRY_PATH, "styles", style.name)
const dependencies = [
"tailwindcss-animate",
"class-variance-authority",
"lucide-react",
]
// TODO: Remove this when we migrate to lucide-react.
if (style.name === "new-york") {
dependencies.push("@radix-ui/react-icons")
}
const payload: RegistryEntry = {
name: style.name,
type: "registry:style",
dependencies: [
"tailwindcss-animate",
"class-variance-authority",
"lucide-react",
// TODO: Remove this when we migrate to lucide-react.
style.name === "new-york" ? "@radix-ui/react-icons" : "",
],
dependencies,
registryDependencies: ["utils"],
tailwind: {
config: {

View File

@@ -25,6 +25,7 @@
"shadcn:dev": "turbo --filter=shadcn dev",
"shadcn": "pnpm --filter=shadcn start:dev",
"shadcn:build": "pnpm --filter=shadcn build",
"shadcn:test": "pnpm --filter=shadcn test",
"docs:build": "pnpm --filter=www build:docs",
"www:dev": "pnpm --filter=www dev",
"www:build": "pnpm --filter=www build",

View File

@@ -1,5 +1,11 @@
# @shadcn/ui
## 0.9.1
### Patch Changes
- [#4797](https://github.com/shadcn-ui/ui/pull/4797) [`207b69f`](https://github.com/shadcn-ui/ui/commit/207b69fe8dd59b10dddc9337d333416976e2a30d) Thanks [@Wiper-R](https://github.com/Wiper-R)! - add scss support
## 0.9.0
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn-ui",
"version": "0.9.0",
"version": "0.9.1",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"

View File

@@ -133,7 +133,7 @@ export async function getProjectType(cwd: string): Promise<ProjectType | null> {
}
export async function getTailwindCssFile(cwd: string) {
const files = await fg.glob("**/*.css", {
const files = await fg.glob(["**/*.css", "**/*.scss"], {
cwd,
deep: 3,
ignore: PROJECT_SHARED_IGNORE,

View File

@@ -1,5 +1,39 @@
# @shadcn/ui
## 2.0.7
### Patch Changes
- [#4848](https://github.com/shadcn-ui/ui/pull/4848) [`1af66c2`](https://github.com/shadcn-ui/ui/commit/1af66c2d08df7dd7f6a8d4d1544d965e41a1fb0d) Thanks [@jherr](https://github.com/jherr)! - add support for ~ dir in target path
- [#4815](https://github.com/shadcn-ui/ui/pull/4815) [`408760a`](https://github.com/shadcn-ui/ui/commit/408760a93b398b7d02a0a522a74a7a195ccda7c4) Thanks [@rana-haris-ali](https://github.com/rana-haris-ali)! - fix typo in error message
## 2.0.6
### Patch Changes
- [#4820](https://github.com/shadcn-ui/ui/pull/4820) [`64b2f1a`](https://github.com/shadcn-ui/ui/commit/64b2f1a5ad865c831045c954fec85e0fec2289e7) Thanks [@shadcn](https://github.com/shadcn)! - add docs support
## 2.0.5
### Patch Changes
- [#4787](https://github.com/shadcn-ui/ui/pull/4787) [`99ff9ca`](https://github.com/shadcn-ui/ui/commit/99ff9caf7180c8c19df130969bdef3d0fb78b218) Thanks [@shadcn](https://github.com/shadcn)! - add src to content for tailwind
## 2.0.4
### Patch Changes
- [#4776](https://github.com/shadcn-ui/ui/pull/4776) [`49373ee`](https://github.com/shadcn-ui/ui/commit/49373eed9672d6ecf82219f6e682cab914e7cc41) Thanks [@shadcn](https://github.com/shadcn)! - better error handling
## 2.0.3
### Patch Changes
- [#4729](https://github.com/shadcn-ui/ui/pull/4729) [`701e116`](https://github.com/shadcn-ui/ui/commit/701e1160ea86b2ddd9f43121b447477caaf7aefa) Thanks [@shadcn](https://github.com/shadcn)! - add --src-dir
- [#4731](https://github.com/shadcn-ui/ui/pull/4731) [`faa7a67`](https://github.com/shadcn-ui/ui/commit/faa7a67fb30c741c2f2559fc64b34f103aac9d79) Thanks [@shadcn](https://github.com/shadcn)! - fix routes for src dir
## 2.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "2.0.0",
"version": "2.0.7",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"
@@ -13,7 +13,7 @@
"repository": {
"type": "git",
"url": "https://github.com/shadcn/ui.git",
"directory": "packages/cli"
"directory": "packages/shadcn"
},
"files": [
"dist"

View File

@@ -21,6 +21,7 @@ export const addOptionsSchema = z.object({
all: z.boolean(),
path: z.string().optional(),
silent: z.boolean(),
srcDir: z.boolean().optional(),
})
export const add = new Command()
@@ -40,6 +41,11 @@ export const add = new Command()
.option("-a, --all", "add all available components", false)
.option("-p, --path <path>", "the path to add the component to.")
.option("-s, --silent", "mute output.", false)
.option(
"--src-dir",
"use the src directory when creating a new project.",
false
)
.action(async (components, opts) => {
try {
const options = addOptionsSchema.parse({
@@ -100,6 +106,7 @@ export const add = new Command()
skipPreflight: false,
silent: true,
isNewProject: false,
srcDir: options.srcDir,
})
}
@@ -108,6 +115,7 @@ export const add = new Command()
const { projectPath } = await createProject({
cwd: options.cwd,
force: options.overwrite,
srcDir: options.srcDir,
})
if (!projectPath) {
logger.break()
@@ -123,6 +131,7 @@ export const add = new Command()
skipPreflight: true,
silent: true,
isNewProject: true,
srcDir: options.srcDir,
})
shouldUpdateAppIndex =

View File

@@ -20,6 +20,7 @@ import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import { getRegistryBaseColors, getRegistryStyles } from "@/src/utils/registry"
import { spinner } from "@/src/utils/spinner"
import { updateTailwindContent } from "@/src/utils/updaters/update-tailwind-content"
import { Command } from "commander"
import prompts from "prompts"
import { z } from "zod"
@@ -32,6 +33,7 @@ export const initOptionsSchema = z.object({
force: z.boolean(),
silent: z.boolean(),
isNewProject: z.boolean(),
srcDir: z.boolean().optional(),
})
export const init = new Command()
@@ -50,6 +52,11 @@ export const init = new Command()
process.cwd()
)
.option("-s, --silent", "mute output.", false)
.option(
"--src-dir",
"use the src directory when creating a new project.",
false
)
.action(async (components, opts) => {
try {
const options = initOptionsSchema.parse({
@@ -131,6 +138,18 @@ export async function runInit(
options.isNewProject || projectInfo?.framework.name === "next-app",
})
// If a new project is using src dir, let's update the tailwind content config.
// TODO: Handle this per framework.
if (options.isNewProject && options.srcDir) {
await updateTailwindContent(
["./src/**/*.{js,ts,jsx,tsx,mdx}"],
fullConfig,
{
silent: options.silent,
}
)
}
return fullConfig
}

View File

@@ -1,5 +1,6 @@
import { type Config } from "@/src/utils/get-config"
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
import { registryResolveItemsTree } from "@/src/utils/registry"
import { spinner } from "@/src/utils/spinner"
import { updateCssVars } from "@/src/utils/updaters/update-css-vars"
@@ -48,4 +49,8 @@ export async function addComponents(
overwrite: options.overwrite,
silent: options.silent,
})
if (tree.docs) {
logger.info(tree.docs)
}
}

View File

@@ -10,15 +10,20 @@ import prompts from "prompts"
import { z } from "zod"
export async function createProject(
options: Pick<z.infer<typeof initOptionsSchema>, "cwd" | "force">
options: Pick<z.infer<typeof initOptionsSchema>, "cwd" | "force" | "srcDir">
) {
options = {
srcDir: false,
...options,
}
if (!options.force) {
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: `The path ${highlighter.info(
options.cwd
)} is empty. Would you like to start a new ${highlighter.info(
)} does not contain a package.json file. Would you like to start a new ${highlighter.info(
"Next.js"
)} project?`,
initial: true,
@@ -81,7 +86,7 @@ export async function createProject(
"--eslint",
"--typescript",
"--app",
"--no-src-dir",
options.srcDir ? "--src-dir" : "--no-src-dir",
"--no-import-alias",
`--use-${packageManager}`,
]
@@ -97,7 +102,7 @@ export async function createProject(
} catch (error) {
logger.break()
logger.error(
`Something went wront creating a new Next.js project. Please try again.`
`Something went wrong creating a new Next.js project. Please try again.`
)
process.exit(1)
}

View File

@@ -164,7 +164,6 @@ async function fetchRegistry(paths: string[]) {
const results = await Promise.all(
paths.map(async (path) => {
const url = getRegistryUrl(path)
console.log("👉", url)
const response = await fetch(url, { agent })
if (!response.ok) {
@@ -175,6 +174,31 @@ async function fetchRegistry(paths: string[]) {
404: "Not found",
500: "Internal server error",
}
if (response.status === 401) {
throw new Error(
`You are not authorized to access the component at ${highlighter.info(
url
)}.\nIf this is a remote registry, you may need to authenticate.`
)
}
if (response.status === 404) {
throw new Error(
`The component at ${highlighter.info(
url
)} was not found.\nIt may not exist at the registry. Please make sure it is a valid component.`
)
}
if (response.status === 403) {
throw new Error(
`You do not have access to the component at ${highlighter.info(
url
)}.\nIf this is a remote registry, you may need to authenticate or a token.`
)
}
const result = await response.json()
const message =
result && typeof result === "object" && "error" in result
@@ -297,6 +321,13 @@ export async function registryResolveItemsTree(
cssVars = deepmerge(cssVars, item.cssVars ?? {})
})
let docs = ""
payload.forEach((item) => {
if (item.docs) {
docs += `${item.docs}\n`
}
})
return registryResolvedItemsTreeSchema.parse({
dependencies: deepmerge.all(
payload.map((item) => item.dependencies ?? [])
@@ -307,6 +338,7 @@ export async function registryResolveItemsTree(
files: deepmerge.all(payload.map((item) => item.files ?? [])),
tailwind,
cssVars,
docs,
})
} catch (error) {
handleError(error)

View File

@@ -46,6 +46,7 @@ export const registryItemSchema = z.object({
tailwind: registryItemTailwindSchema.optional(),
cssVars: registryItemCssVarsSchema.optional(),
meta: z.record(z.string(), z.any()).optional(),
docs: z.string().optional(),
})
export type RegistryItem = z.infer<typeof registryItemSchema>
@@ -82,4 +83,5 @@ export const registryResolvedItemsTreeSchema = registryItemSchema.pick({
files: true,
tailwind: true,
cssVars: true,
docs: true,
})

View File

@@ -1,6 +1,7 @@
import { existsSync, promises as fs } from "fs"
import path, { basename } from "path"
import { Config } from "@/src/utils/get-config"
import { getProjectInfo } from "@/src/utils/get-project-info"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import {
@@ -16,6 +17,19 @@ import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
import prompts from "prompts"
export function resolveTargetDir(
projectInfo: Awaited<ReturnType<typeof getProjectInfo>>,
config: Config,
target: string
) {
if (target.startsWith("~/")) {
return path.join(config.resolvedPaths.cwd, target.replace("~/", ""))
}
return projectInfo?.isSrcDir
? path.join(config.resolvedPaths.cwd, "src", target)
: path.join(config.resolvedPaths.cwd, target)
}
export async function updateFiles(
files: RegistryItem["files"],
config: Config,
@@ -37,7 +51,12 @@ export async function updateFiles(
const filesCreatedSpinner = spinner(`Updating files.`, {
silent: options.silent,
})?.start()
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
const [projectInfo, baseColor] = await Promise.all([
getProjectInfo(config.resolvedPaths.cwd),
getRegistryBaseColor(config.tailwind.baseColor),
])
const filesCreated = []
const filesUpdated = []
const filesSkipped = []
@@ -52,7 +71,7 @@ export async function updateFiles(
let filePath = path.join(targetDir, fileName)
if (file.target) {
filePath = path.join(config.resolvedPaths.cwd, file.target)
filePath = resolveTargetDir(projectInfo, config, file.target)
targetDir = path.dirname(filePath)
}

View File

@@ -196,12 +196,14 @@ async function addTailwindConfigTheme(
const themeObject = await parseObjectLiteral(themeObjectString)
const result = deepmerge(themeObject, theme)
const resultString = objectToString(result)
.replace(/\'\"/g, "'")
.replace(/\"\'/g, "'")
.replace(/\'\[/g, "[")
.replace(/\]\'/g, "]")
.replace(/\\\'/g, "")
.replace(/\\\'/g, "")
.replace(/\'\"/g, "'") // Replace `\" with "
.replace(/\"\'/g, "'") // Replace `\" with "
.replace(/\'\[/g, "[") // Replace `[ with [
.replace(/\]\'/g, "]") // Replace `] with ]
.replace(/\'\\\'/g, "'") // Replace `\' with '
.replace(/\\\'/g, "'") // Replace \' with '
.replace(/\\\'\'/g, "'")
.replace(/\'\'/g, "'")
themeInitializer.replaceWithText(resultString)
}
@@ -247,7 +249,7 @@ function addTailwindConfigPlugin(
return configObject
}
async function _createSourceFile(input: string, config: Config | null) {
export async function _createSourceFile(input: string, config: Config | null) {
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
const resolvedPath =
config?.resolvedPaths?.tailwindConfig || "tailwind.config.ts"
@@ -266,7 +268,7 @@ async function _createSourceFile(input: string, config: Config | null) {
return sourceFile
}
function _getQuoteChar(configObject: ObjectLiteralExpression) {
export function _getQuoteChar(configObject: ObjectLiteralExpression) {
return configObject
.getFirstDescendantByKind(SyntaxKind.StringLiteral)
?.getQuoteKind() === QuoteKind.Single

View File

@@ -0,0 +1,121 @@
import { promises as fs } from "fs"
import path from "path"
import { Config } from "@/src/utils/get-config"
import { highlighter } from "@/src/utils/highlighter"
import { spinner } from "@/src/utils/spinner"
import {
_createSourceFile,
_getQuoteChar,
} from "@/src/utils/updaters/update-tailwind-config"
import { ObjectLiteralExpression, SyntaxKind } from "ts-morph"
export async function updateTailwindContent(
content: string[],
config: Config,
options: {
silent?: boolean
}
) {
if (!content) {
return
}
options = {
silent: false,
...options,
}
const tailwindFileRelativePath = path.relative(
config.resolvedPaths.cwd,
config.resolvedPaths.tailwindConfig
)
const tailwindSpinner = spinner(
`Updating ${highlighter.info(tailwindFileRelativePath)}`,
{
silent: options.silent,
}
).start()
const raw = await fs.readFile(config.resolvedPaths.tailwindConfig, "utf8")
const output = await transformTailwindContent(raw, content, config)
await fs.writeFile(config.resolvedPaths.tailwindConfig, output, "utf8")
tailwindSpinner?.succeed()
}
export async function transformTailwindContent(
input: string,
content: string[],
config: Config
) {
const sourceFile = await _createSourceFile(input, config)
// Find the object with content property.
// This is faster than traversing the default export.
// TODO: maybe we do need to traverse the default export?
const configObject = sourceFile
.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)
.find((node) =>
node
.getProperties()
.some(
(property) =>
property.isKind(SyntaxKind.PropertyAssignment) &&
property.getName() === "content"
)
)
// We couldn't find the config object, so we return the input as is.
if (!configObject) {
return input
}
addTailwindConfigContent(configObject, content)
return sourceFile.getFullText()
}
async function addTailwindConfigContent(
configObject: ObjectLiteralExpression,
content: string[]
) {
const quoteChar = _getQuoteChar(configObject)
const existingProperty = configObject.getProperty("content")
if (!existingProperty) {
const newProperty = {
name: "content",
initializer: `[${quoteChar}${content.join(
`${quoteChar}, ${quoteChar}`
)}${quoteChar}]`,
}
configObject.addPropertyAssignment(newProperty)
return configObject
}
if (existingProperty.isKind(SyntaxKind.PropertyAssignment)) {
const initializer = existingProperty.getInitializer()
// If property is an array, append.
if (initializer?.isKind(SyntaxKind.ArrayLiteralExpression)) {
for (const contentItem of content) {
const newValue = `${quoteChar}${contentItem}${quoteChar}`
// Check if the array already contains the value.
if (
initializer
.getElements()
.map((element) => element.getText())
.includes(newValue)
) {
continue
}
initializer.addElement(newValue)
}
}
return configObject
}
return configObject
}

View File

@@ -12,16 +12,15 @@ exports[`registryResolveItemTree > should resolve index 1`] = `
"tailwindcss-animate",
"class-variance-authority",
"lucide-react",
"",
"tailwindcss-animate",
"class-variance-authority",
"lucide-react",
"",
"@radix-ui/react-label",
"clsx",
"tailwind-merge",
],
"devDependencies": [],
"docs": "",
"files": [
{
"content": ""use client"
@@ -95,6 +94,7 @@ exports[`registryResolveItemTree > should resolve items tree 1`] = `
"@radix-ui/react-slot",
],
"devDependencies": [],
"docs": "",
"files": [
{
"content": "import * as React from "react"
@@ -173,6 +173,7 @@ exports[`registryResolveItemTree > should resolve multiple items tree 1`] = `
"@radix-ui/react-dialog",
],
"devDependencies": [],
"docs": "",
"files": [
{
"content": "import * as React from "react"

View File

@@ -369,6 +369,41 @@ export default config
"
`;
exports[`transformTailwindConfig -> theme > should keep quotes in strings 1`] = `
"import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans]
},
colors: {
...defaultColors,
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
}
}
}
},
}
export default config
"
`;
exports[`transformTailwindConfig -> theme > should keep spread assignments 1`] = `
"import type { Config } from 'tailwindcss'

View File

@@ -0,0 +1,52 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`transformTailwindContent -> content property > should NOT add content property if already in config 1`] = `
"import type { Config } from 'tailwindcss'
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./bar/**/*.{js,ts,jsx,tsx,mdx}"
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
}
export default config
"
`;
exports[`transformTailwindContent -> content property > should add content property if not in config 1`] = `
"import type { Config } from 'tailwindcss'
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./foo/**/*.{js,ts,jsx,tsx,mdx}",
"./bar/**/*.{js,ts,jsx,tsx,mdx}"
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
}
export default config
"
`;

View File

@@ -0,0 +1,65 @@
import { describe, expect, test } from "vitest"
import { resolveTargetDir } from "../../../src/utils/updaters/update-files"
describe("resolveTargetDir", () => {
test("should handle a home target without a src directory", () => {
const targetDir = resolveTargetDir(
{
isSrcDir: false,
},
{
resolvedPaths: {
cwd: "/foo/bar",
},
},
"~/.env"
)
expect(targetDir).toBe("/foo/bar/.env")
})
test("should handle a home target even with a src directory", () => {
const targetDir = resolveTargetDir(
{
isSrcDir: true,
},
{
resolvedPaths: {
cwd: "/foo/bar",
},
},
"~/.env"
)
expect(targetDir).toBe("/foo/bar/.env")
})
test("should handle a simple target", () => {
const targetDir = resolveTargetDir(
{
isSrcDir: false,
},
{
resolvedPaths: {
cwd: "/foo/bar",
},
},
"./components/ui/button.tsx"
)
expect(targetDir).toBe("/foo/bar/components/ui/button.tsx")
})
test("should handle a simple target with src directory", () => {
const targetDir = resolveTargetDir(
{
isSrcDir: true,
},
{
resolvedPaths: {
cwd: "/foo/bar",
},
},
"./components/ui/button.tsx"
)
expect(targetDir).toBe("/foo/bar/src/components/ui/button.tsx")
})
})

View File

@@ -760,6 +760,55 @@ export default config
expect(output3).toBe(output1)
expect(output3).toBe(output2)
})
test("should keep quotes in strings", async () => {
expect(
await transformTailwindConfig(
`import type { Config } from 'tailwindcss'
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
colors: {
...defaultColors,
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
},
},
},
}
export default config
`,
{
theme: {
extend: {
colors: {
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
},
},
},
{
config: SHARED_CONFIG,
}
)
).toMatchSnapshot()
})
})
describe("nestSpreadProperties", () => {

View File

@@ -0,0 +1,94 @@
import { describe, expect, test } from "vitest"
import { transformTailwindContent } from "../../../src/utils/updaters/update-tailwind-content"
const SHARED_CONFIG = {
$schema: "https://ui.shadcn.com/schema.json",
style: "new-york",
rsc: true,
tsx: true,
tailwind: {
config: "tailwind.config.ts",
css: "app/globals.css",
baseColor: "slate",
cssVariables: true,
},
aliases: {
components: "@/components",
utils: "@/lib/utils",
},
resolvedPaths: {
cwd: ".",
tailwindConfig: "tailwind.config.ts",
tailwindCss: "app/globals.css",
components: "./components",
utils: "./lib/utils",
ui: "./components/ui",
},
}
describe("transformTailwindContent -> content property", () => {
test("should add content property if not in config", async () => {
expect(
await transformTailwindContent(
`import type { Config } from 'tailwindcss'
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
}
export default config
`,
["./foo/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"],
{
config: SHARED_CONFIG,
}
)
).toMatchSnapshot()
})
test("should NOT add content property if already in config", async () => {
expect(
await transformTailwindContent(
`import type { Config } from 'tailwindcss'
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
}
export default config
`,
["./app/**/*.{js,ts,jsx,tsx,mdx}", "./bar/**/*.{js,ts,jsx,tsx,mdx}"],
{
config: SHARED_CONFIG,
}
)
).toMatchSnapshot()
})
})