mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-17 21:01:51 +00:00
Compare commits
6 Commits
shadcn@2.0
...
shadcn@2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ca57a79c | ||
|
|
99ff9caf71 | ||
|
|
cd9a55b76a | ||
|
|
49373eed96 | ||
|
|
078dfe6607 | ||
|
|
77fc5ec8db |
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
35
apps/www/components/open-in-v0-cta.tsx
Normal file
35
apps/www/components/open-in-v0-cta.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 "@/components/hooks/use-toast"
|
||||
```
|
||||
|
||||
```tsx {2,7-10}
|
||||
|
||||
30
apps/www/content/docs/v0.mdx
Normal file
30
apps/www/content/docs/v0.mdx
Normal 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).
|
||||
BIN
apps/www/public/images/open-in-v0-dark.png
Normal file
BIN
apps/www/public/images/open-in-v0-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
apps/www/public/images/open-in-v0.png
Normal file
BIN
apps/www/public/images/open-in-v0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
@@ -1,5 +1,17 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "2.0.3",
|
||||
"version": "2.0.5",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -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"
|
||||
@@ -137,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
|
||||
}
|
||||
|
||||
|
||||
@@ -174,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
|
||||
|
||||
@@ -249,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"
|
||||
@@ -268,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
|
||||
|
||||
121
packages/shadcn/src/utils/updaters/update-tailwind-content.ts
Normal file
121
packages/shadcn/src/utils/updaters/update-tailwind-content.ts
Normal 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
|
||||
}
|
||||
@@ -12,11 +12,9 @@ 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",
|
||||
|
||||
@@ -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
|
||||
"
|
||||
`;
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user