mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-27 14:44:12 +00:00
Compare commits
5 Commits
shadcn/cli
...
shadcn/01-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e1940525 | ||
|
|
074eed5605 | ||
|
|
ca7fbc3b64 | ||
|
|
b3b2fe2755 | ||
|
|
1fcb318c56 |
5
.changeset/warm-fans-protect.md
Normal file
5
.changeset/warm-fans-protect.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": minor
|
||||
---
|
||||
|
||||
add theme prop to registry-item schema
|
||||
@@ -240,7 +240,7 @@
|
||||
"name": "command",
|
||||
"type": "registry:ui",
|
||||
"dependencies": [
|
||||
"cmdk@1.0.0"
|
||||
"cmdk"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"dialog"
|
||||
|
||||
@@ -14,7 +14,7 @@ const badgeVariants = cva(
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70",
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ function NavigationMenuItem({
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
@@ -129,7 +129,7 @@ function NavigationMenuLink({
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -18,7 +18,7 @@ function ScrollArea({
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
|
||||
@@ -49,6 +49,46 @@ Here's an example of a complex component that installs a page, two components, a
|
||||
|
||||
### How do I add a new Tailwind color?
|
||||
|
||||
<Tabs defaultValue="v4">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="v4">Tailwind CSS v4</TabsTrigger>
|
||||
<TabsTrigger value="v3">Tailwind CSS v3</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="v4">
|
||||
|
||||
To add a new color you need to add it to `cssVars` under `light` and `dark` keys.
|
||||
|
||||
```json showLineNumbers {10-18}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "hello-world",
|
||||
"title": "Hello World",
|
||||
"type": "registry:block",
|
||||
"description": "A complex hello world component",
|
||||
"files": [
|
||||
// ...
|
||||
],
|
||||
"cssVars": {
|
||||
"light": {
|
||||
"brand-background": "20 14.3% 4.1%",
|
||||
"brand-accent": "20 14.3% 4.1%"
|
||||
},
|
||||
"dark": {
|
||||
"brand-background": "20 14.3% 4.1%",
|
||||
"brand-accent": "20 14.3% 4.1%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The CLI will update the project CSS file. Once updated, the new colors will be available to be used as utility classes: `bg-brand` and `text-brand-accent`.
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="v3">
|
||||
|
||||
To add a new color you need to add it to `cssVars` and `tailwind.config.theme.extend.colors`.
|
||||
|
||||
```json showLineNumbers {10-19} {24-29}
|
||||
@@ -90,9 +130,47 @@ To add a new color you need to add it to `cssVars` and `tailwind.config.theme.ex
|
||||
|
||||
The CLI will update the project CSS file and tailwind.config.js file. Once updated, the new colors will be available to be used as utility classes: `bg-brand` and `text-brand-accent`.
|
||||
|
||||
### How do I add a Tailwind animation?
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
To add a new animation you add it to `tailwind.config.theme.extend.animation` and `tailwind.config.theme.extend.keyframes`.
|
||||
### How do I add or override a Tailwind theme variable?
|
||||
|
||||
<Tabs defaultValue="v4">
|
||||
|
||||
<TabsList>
|
||||
<TabsTrigger value="v4">Tailwind CSS v4</TabsTrigger>
|
||||
<TabsTrigger value="v3">Tailwind CSS v3</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="v4">
|
||||
|
||||
To add or override a theme variable you add it to `cssVars.theme` under the key you want to add or override.
|
||||
|
||||
```json showLineNumbers {10-15}
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||
"name": "hello-world",
|
||||
"title": "Hello World",
|
||||
"type": "registry:block",
|
||||
"description": "A complex hello world component",
|
||||
"files": [
|
||||
// ...
|
||||
],
|
||||
"cssVars": {
|
||||
"theme": {
|
||||
"text-base": "3rem",
|
||||
"ease-in-out": "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
"font-heading": "Poppins, sans-serif"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="v3">
|
||||
|
||||
To override a theme variable you add it to `tailwind.config.theme.extend` under the key you want to override.
|
||||
|
||||
```json showLineNumbers {14-22}
|
||||
{
|
||||
@@ -108,14 +186,8 @@ To add a new animation you add it to `tailwind.config.theme.extend.animation` an
|
||||
"config": {
|
||||
"theme": {
|
||||
"extend": {
|
||||
"keyframes": {
|
||||
"wiggle": {
|
||||
"0%, 100%": { "transform": "rotate(-3deg)" },
|
||||
"50%": { "transform": "rotate(3deg)" }
|
||||
}
|
||||
},
|
||||
"animation": {
|
||||
"wiggle": "wiggle 1s ease-in-out infinite"
|
||||
"text": {
|
||||
"base": "3rem"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,3 +195,6 @@ To add a new animation you add it to `tailwind.config.theme.extend.animation` an
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -21,7 +21,18 @@ The `registry-item.json` schema is used to define your custom registry items.
|
||||
"path": "registry/new-york/hello-world/use-hello-world.ts",
|
||||
"type": "registry:hook"
|
||||
}
|
||||
]
|
||||
],
|
||||
"cssVars": {
|
||||
"theme": {
|
||||
"font-heading": "Poppins, sans-serif"
|
||||
},
|
||||
"light": {
|
||||
"brand": "20 14.3% 4.1%"
|
||||
},
|
||||
"dark": {
|
||||
"brand": "20 14.3% 4.1%"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -41,7 +52,7 @@ The `$schema` property is used to specify the schema for the `registry-item.json
|
||||
|
||||
### name
|
||||
|
||||
The `name` property is used to specify the name of your registry item.
|
||||
The name of the item. This is used to identify the item in the registry. It should be unique for your registry.
|
||||
|
||||
```json title="registry-item.json" showLineNumbers
|
||||
{
|
||||
@@ -71,7 +82,7 @@ A description of your registry item. This can be longer and more detailed than t
|
||||
|
||||
### type
|
||||
|
||||
The `type` property is used to specify the type of your registry item.
|
||||
The `type` property is used to specify the type of your registry item. This is used to determine the type and target path of the item when resolved for a project.
|
||||
|
||||
```json title="registry-item.json" showLineNumbers
|
||||
{
|
||||
@@ -90,6 +101,8 @@ The following types are supported:
|
||||
| `registry:ui` | Use for UI components and single-file primitives |
|
||||
| `registry:page` | Use for page or file-based routes. |
|
||||
| `registry:file` | Use for miscellaneous files. |
|
||||
| `registry:style` | Use for registry styles. eg. `new-york` |
|
||||
| `registry:theme` | Use for themes. |
|
||||
|
||||
### author
|
||||
|
||||
@@ -122,7 +135,7 @@ Use `@version` to specify the version of your registry item.
|
||||
|
||||
### registryDependencies
|
||||
|
||||
Used for registry dependencies. Can be names or URLs.
|
||||
Used for registry dependencies. Can be names or URLs. Use the name of the item to reference shadcn/ui components and urls to reference other registries.
|
||||
|
||||
- For `shadcn/ui` registry items such as `button`, `input`, `select`, etc use the name eg. `['button', 'input', 'select']`.
|
||||
- For custom registry items use the URL of the registry item eg. `['https://example.com/r/hello-world.json']`.
|
||||
@@ -189,6 +202,8 @@ Use `~` to refer to the root of the project e.g `~/foo.config.js`.
|
||||
|
||||
### tailwind
|
||||
|
||||
**DEPRECATED:** Use `cssVars.theme` instead for Tailwind v4 projects.
|
||||
|
||||
The `tailwind` property is used for tailwind configuration such as `theme`, `plugins` and `content`.
|
||||
|
||||
You can use the `tailwind.config` property to add colors, animations and plugins to your registry item.
|
||||
@@ -225,6 +240,9 @@ Use to define CSS variables for your registry item.
|
||||
```json title="registry-item.json" showLineNumbers
|
||||
{
|
||||
"cssVars": {
|
||||
"theme": {
|
||||
"font-heading": "Poppins, sans-serif"
|
||||
},
|
||||
"light": {
|
||||
"brand": "20 14.3% 4.1%",
|
||||
"radius": "0.5rem"
|
||||
@@ -236,11 +254,6 @@ Use to define CSS variables for your registry item.
|
||||
}
|
||||
```
|
||||
|
||||
<Callout>
|
||||
**Note:** When adding colors, make sure to also add them to the
|
||||
`tailwind.config.theme.extend.colors` property.
|
||||
</Callout>
|
||||
|
||||
### docs
|
||||
|
||||
Use `docs` to show custom documentation or message when installing your registry item via the CLI.
|
||||
|
||||
@@ -92,6 +92,7 @@ Here's the list of variables available for customization:
|
||||
|
||||
```css title="app/globals.css"
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
@@ -107,7 +108,6 @@ Here's the list of variables available for customization:
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: 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);
|
||||
@@ -116,7 +116,6 @@ Here's the list of variables available for customization:
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
@@ -130,22 +129,21 @@ Here's the list of variables available for customization:
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover: oklch(0.269 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: 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: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 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.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
@@ -158,7 +156,7 @@ Here's the list of variables available for customization:
|
||||
--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(0.269 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
"name": "command",
|
||||
"type": "registry:ui",
|
||||
"dependencies": [
|
||||
"cmdk@1.0.0"
|
||||
"cmdk"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"dialog"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "registry:ui",
|
||||
"author": "shadcn (https://ui.shadcn.com)",
|
||||
"dependencies": [
|
||||
"cmdk@1.0.0"
|
||||
"cmdk"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"dialog"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/badge.tsx",
|
||||
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n",
|
||||
"content": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "command",
|
||||
"type": "registry:ui",
|
||||
"dependencies": [
|
||||
"cmdk@1.0.0"
|
||||
"cmdk"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"dialog"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york-v4/ui/scroll-area.tsx",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ScrollArea({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n return (\n <ScrollAreaPrimitive.Root\n data-slot=\"scroll-area\"\n className={cn(\"relative\", className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n data-slot=\"scroll-area-viewport\"\n className=\"ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n <ScrollBar />\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n )\n}\n\nfunction ScrollBar({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n return (\n <ScrollAreaPrimitive.ScrollAreaScrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation={orientation}\n className={cn(\n \"flex touch-none p-px transition-colors select-none\",\n orientation === \"vertical\" &&\n \"h-full w-2.5 border-l border-l-transparent\",\n orientation === \"horizontal\" &&\n \"h-2.5 flex-col border-t border-t-transparent\",\n className\n )}\n {...props}\n >\n <ScrollAreaPrimitive.ScrollAreaThumb\n data-slot=\"scroll-area-thumb\"\n className=\"bg-border relative flex-1 rounded-full\"\n />\n </ScrollAreaPrimitive.ScrollAreaScrollbar>\n )\n}\n\nexport { ScrollArea, ScrollBar }\n",
|
||||
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ScrollArea({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n return (\n <ScrollAreaPrimitive.Root\n data-slot=\"scroll-area\"\n className={cn(\"relative\", className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n data-slot=\"scroll-area-viewport\"\n className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n <ScrollBar />\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n )\n}\n\nfunction ScrollBar({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n return (\n <ScrollAreaPrimitive.ScrollAreaScrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation={orientation}\n className={cn(\n \"flex touch-none p-px transition-colors select-none\",\n orientation === \"vertical\" &&\n \"h-full w-2.5 border-l border-l-transparent\",\n orientation === \"horizontal\" &&\n \"h-2.5 flex-col border-t border-t-transparent\",\n className\n )}\n {...props}\n >\n <ScrollAreaPrimitive.ScrollAreaThumb\n data-slot=\"scroll-area-thumb\"\n className=\"bg-border relative flex-1 rounded-full\"\n />\n </ScrollAreaPrimitive.ScrollAreaScrollbar>\n )\n}\n\nexport { ScrollArea, ScrollBar }\n",
|
||||
"type": "registry:ui"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"type": "registry:ui",
|
||||
"author": "shadcn (https://ui.shadcn.com)",
|
||||
"dependencies": [
|
||||
"cmdk@1.0.0"
|
||||
"cmdk"
|
||||
],
|
||||
"registryDependencies": [
|
||||
"dialog"
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The name of the item. This is used to identify the item in the registry. It should be unique for your registry."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
@@ -15,46 +16,57 @@
|
||||
"registry:hook",
|
||||
"registry:theme",
|
||||
"registry:page",
|
||||
"registry:file"
|
||||
]
|
||||
"registry:file",
|
||||
"registry:style"
|
||||
],
|
||||
"description": "The type of the item. This is used to determine the type and target path of the item when resolved for a project."
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The description of the item. This is used to provide a brief overview of the item."
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The human-readable title for your registry item. Keep it short and descriptive."
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The author of the item. Recommended format: username <url>"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"description": "An array of NPM dependencies required by the registry item.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"type": "array",
|
||||
"description": "An array of NPM dev dependencies required by the registry item.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"registryDependencies": {
|
||||
"type": "array",
|
||||
"description": "An array of registry items that this item depends on. Use the name of the item to reference shadcn/ui components and urls to reference other registries.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"description": "The main payload of the registry item. This is an array of files that are part of the registry item. Each file is an object with a path, content, type, and target.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The path to the file relative to the registry root."
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The content of the file."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
@@ -67,10 +79,12 @@
|
||||
"registry:theme",
|
||||
"registry:page",
|
||||
"registry:file"
|
||||
]
|
||||
],
|
||||
"description": "The type of the file. This is used to determine the type of the file when resolved for a project."
|
||||
},
|
||||
"target": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The target path of the file. This is the path to the file in the project."
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
@@ -90,6 +104,7 @@
|
||||
},
|
||||
"tailwind": {
|
||||
"type": "object",
|
||||
"description": "The tailwind configuration for the registry item. This is an object with a config property. Use cssVars for Tailwind v4 projects.",
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "object",
|
||||
@@ -116,15 +131,25 @@
|
||||
},
|
||||
"cssVars": {
|
||||
"type": "object",
|
||||
"description": "The css variables for the registry item. This will be merged with the project's css variables.",
|
||||
"properties": {
|
||||
"theme": {
|
||||
"type": "object",
|
||||
"description": "CSS variables for the @theme directive. For Tailwind v4 projects only. Use tailwind for older projects.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"type": "object",
|
||||
"description": "CSS variables for the light theme.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"type": "object",
|
||||
"description": "CSS variables for the dark theme.",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -133,16 +158,23 @@
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"description": "Additional metadata for the registry item. This is an object with any key value pairs.",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"docs": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The documentation for the registry item. This is a markdown string."
|
||||
},
|
||||
"categories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "The categories of the registry item. This is an array of strings."
|
||||
}
|
||||
},
|
||||
"extends": {
|
||||
"type": "string",
|
||||
"description": "The name of the registry item to extend. This is used to extend the base shadcn/ui style. Set to none to start fresh. This is available for registry:style items only."
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"]
|
||||
|
||||
@@ -182,7 +182,7 @@ export const ui: Registry["items"] = [
|
||||
{
|
||||
name: "command",
|
||||
type: "registry:ui",
|
||||
dependencies: ["cmdk@1.0.0"],
|
||||
dependencies: ["cmdk"],
|
||||
registryDependencies: ["dialog"],
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -148,6 +148,7 @@ export const add = new Command()
|
||||
isNewProject: false,
|
||||
srcDir: options.srcDir,
|
||||
cssVariables: options.cssVariables,
|
||||
style: "index",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,6 +180,7 @@ export const add = new Command()
|
||||
isNewProject: true,
|
||||
srcDir: options.srcDir,
|
||||
cssVariables: options.cssVariables,
|
||||
style: "index",
|
||||
})
|
||||
|
||||
shouldUpdateAppIndex =
|
||||
|
||||
@@ -4,6 +4,7 @@ import { preFlightInit } from "@/src/preflights/preflight-init"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getRegistryBaseColors,
|
||||
getRegistryItem,
|
||||
getRegistryStyles,
|
||||
} from "@/src/registry/api"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
@@ -74,6 +75,7 @@ export const initOptionsSchema = z.object({
|
||||
).join("', '")}'`,
|
||||
}
|
||||
),
|
||||
style: z.string(),
|
||||
})
|
||||
|
||||
export const init = new Command()
|
||||
@@ -118,9 +120,22 @@ export const init = new Command()
|
||||
cwd: path.resolve(opts.cwd),
|
||||
isNewProject: false,
|
||||
components,
|
||||
style: "index",
|
||||
...opts,
|
||||
})
|
||||
|
||||
// We need to check if we're initializing with a new style.
|
||||
// We fetch the payload of the first item.
|
||||
// This is okay since the request is cached and deduped.
|
||||
const item = await getRegistryItem(components[0], "")
|
||||
|
||||
// Skip base color if style.
|
||||
// We set a default and let the style override it.
|
||||
if (item?.type === "registry:style") {
|
||||
options.baseColor = "neutral"
|
||||
options.style = item.extends ?? "index"
|
||||
}
|
||||
|
||||
await runInit(options)
|
||||
|
||||
logger.log(
|
||||
@@ -191,11 +206,15 @@ export async function runInit(
|
||||
|
||||
// Add components.
|
||||
const fullConfig = await resolveConfigPaths(options.cwd, config)
|
||||
const components = ["index", ...(options.components || [])]
|
||||
const components = [
|
||||
...(options.style === "none" ? [] : [options.style]),
|
||||
...(options.components ?? []),
|
||||
]
|
||||
await addComponents(components, fullConfig, {
|
||||
// Init will always overwrite files.
|
||||
overwrite: true,
|
||||
silent: options.silent,
|
||||
style: options.style,
|
||||
isNewProject:
|
||||
options.isNewProject || projectInfo?.framework.name === "next-app",
|
||||
})
|
||||
|
||||
@@ -289,6 +289,14 @@ export async function registryResolveItemsTree(
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the payload so that registry:theme is always first.
|
||||
payload.sort((a, b) => {
|
||||
if (a.type === "registry:theme") {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
|
||||
let tailwind = {}
|
||||
payload.forEach((item) => {
|
||||
tailwind = deepmerge(tailwind, item.tailwind ?? {})
|
||||
@@ -396,6 +404,7 @@ export async function registryGetTheme(name: string, config: Config) {
|
||||
},
|
||||
},
|
||||
cssVars: {
|
||||
theme: {},
|
||||
light: {
|
||||
radius: "0.5rem",
|
||||
},
|
||||
@@ -406,9 +415,13 @@ export async function registryGetTheme(name: string, config: Config) {
|
||||
if (config.tailwind.cssVariables) {
|
||||
theme.tailwind.config.theme.extend.colors = {
|
||||
...theme.tailwind.config.theme.extend.colors,
|
||||
...buildTailwindThemeColorsFromCssVars(baseColor.cssVars.dark),
|
||||
...buildTailwindThemeColorsFromCssVars(baseColor.cssVars.dark ?? {}),
|
||||
}
|
||||
theme.cssVars = {
|
||||
theme: {
|
||||
...baseColor.cssVars.theme,
|
||||
...theme.cssVars.theme,
|
||||
},
|
||||
light: {
|
||||
...baseColor.cssVars.light,
|
||||
...theme.cssVars.light,
|
||||
@@ -421,6 +434,10 @@ export async function registryGetTheme(name: string, config: Config) {
|
||||
|
||||
if (tailwindVersion === "v4" && baseColor.cssVarsV4) {
|
||||
theme.cssVars = {
|
||||
theme: {
|
||||
...baseColor.cssVarsV4.theme,
|
||||
...theme.cssVars.theme,
|
||||
},
|
||||
light: {
|
||||
radius: "0.625rem",
|
||||
...baseColor.cssVarsV4.light,
|
||||
|
||||
@@ -11,11 +11,11 @@ export const registryItemTypeSchema = z.enum([
|
||||
"registry:hook",
|
||||
"registry:page",
|
||||
"registry:file",
|
||||
"registry:theme",
|
||||
"registry:style",
|
||||
|
||||
// Internal use only
|
||||
"registry:theme",
|
||||
"registry:example",
|
||||
"registry:style",
|
||||
"registry:internal",
|
||||
])
|
||||
|
||||
@@ -46,12 +46,14 @@ export const registryItemTailwindSchema = z.object({
|
||||
})
|
||||
|
||||
export const registryItemCssVarsSchema = z.object({
|
||||
theme: z.record(z.string(), z.string()).optional(),
|
||||
light: z.record(z.string(), z.string()).optional(),
|
||||
dark: z.record(z.string(), z.string()).optional(),
|
||||
})
|
||||
|
||||
export const registryItemSchema = z.object({
|
||||
$schema: z.string().optional(),
|
||||
extends: z.string().optional(),
|
||||
name: z.string(),
|
||||
type: registryItemTypeSchema,
|
||||
title: z.string().optional(),
|
||||
@@ -97,16 +99,8 @@ export const registryBaseColorSchema = z.object({
|
||||
light: z.record(z.string(), z.string()),
|
||||
dark: z.record(z.string(), z.string()),
|
||||
}),
|
||||
cssVars: z.object({
|
||||
light: z.record(z.string(), z.string()),
|
||||
dark: z.record(z.string(), z.string()),
|
||||
}),
|
||||
cssVarsV4: z
|
||||
.object({
|
||||
light: z.record(z.string(), z.string()),
|
||||
dark: z.record(z.string(), z.string()),
|
||||
})
|
||||
.optional(),
|
||||
cssVars: registryItemCssVarsSchema,
|
||||
cssVarsV4: registryItemCssVarsSchema.optional(),
|
||||
inlineColorsTemplate: z.string(),
|
||||
cssVarsTemplate: z.string(),
|
||||
})
|
||||
|
||||
@@ -32,12 +32,14 @@ export async function addComponents(
|
||||
overwrite?: boolean
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
style?: string
|
||||
}
|
||||
) {
|
||||
options = {
|
||||
overwrite: false,
|
||||
silent: false,
|
||||
isNewProject: false,
|
||||
style: "index",
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -64,12 +66,14 @@ async function addProjectComponents(
|
||||
overwrite?: boolean
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
style?: string
|
||||
}
|
||||
) {
|
||||
const registrySpinner = spinner(`Checking registry.`, {
|
||||
silent: options.silent,
|
||||
})?.start()
|
||||
const tree = await registryResolveItemsTree(components, config)
|
||||
|
||||
if (!tree) {
|
||||
registrySpinner?.fail()
|
||||
return handleError(new Error("Failed to fetch components from registry."))
|
||||
@@ -82,11 +86,15 @@ async function addProjectComponents(
|
||||
silent: options.silent,
|
||||
tailwindVersion,
|
||||
})
|
||||
|
||||
const overwriteCssVars = await shouldOverwriteCssVars(components, config)
|
||||
await updateCssVars(tree.cssVars, config, {
|
||||
cleanupDefaultNextStyles: options.isNewProject,
|
||||
silent: options.silent,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
initIndex: options.style ? options.style === "index" : false,
|
||||
})
|
||||
|
||||
await updateDependencies(tree.dependencies, config, {
|
||||
@@ -111,6 +119,7 @@ async function addWorkspaceComponents(
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
isRemote?: boolean
|
||||
style?: string
|
||||
}
|
||||
) {
|
||||
const registrySpinner = spinner(`Checking registry.`, {
|
||||
@@ -175,10 +184,12 @@ async function addWorkspaceComponents(
|
||||
|
||||
// 2. Update css vars.
|
||||
if (component.cssVars) {
|
||||
const overwriteCssVars = await shouldOverwriteCssVars(components, config)
|
||||
await updateCssVars(component.cssVars, targetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
tailwindConfig: component.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss)
|
||||
@@ -271,3 +282,17 @@ async function addWorkspaceComponents(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldOverwriteCssVars(
|
||||
components: z.infer<typeof registryItemSchema>["name"][],
|
||||
config: z.infer<typeof configSchema>
|
||||
) {
|
||||
let registryItems = await resolveRegistryItems(components, config)
|
||||
let result = await fetchRegistry(registryItems)
|
||||
const payload = z.array(registryItemSchema).parse(result)
|
||||
|
||||
return payload.some(
|
||||
(component) =>
|
||||
component.type === "registry:theme" || component.type === "registry:style"
|
||||
)
|
||||
}
|
||||
|
||||
78
packages/shadcn/src/utils/highlighter.test.ts
Normal file
78
packages/shadcn/src/utils/highlighter.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { highlighter } from "../../src/utils/highlighter"
|
||||
import { generateRandomString } from "../../test/fuzz-utils"
|
||||
|
||||
describe("fuzzing", () => {
|
||||
test("should handle various input strings", () => {
|
||||
const testCases = Array.from({ length: 100 }, () => ({
|
||||
text: generateRandomString(Math.floor(Math.random() * 100) + 1),
|
||||
}))
|
||||
|
||||
for (const { text } of testCases) {
|
||||
try {
|
||||
// Test each highlighter function
|
||||
const errorResult = highlighter.error(text)
|
||||
const warnResult = highlighter.warn(text)
|
||||
const infoResult = highlighter.info(text)
|
||||
const successResult = highlighter.success(text)
|
||||
|
||||
// All results should be strings
|
||||
expect(typeof errorResult).toBe("string")
|
||||
expect(typeof warnResult).toBe("string")
|
||||
expect(typeof infoResult).toBe("string")
|
||||
expect(typeof successResult).toBe("string")
|
||||
|
||||
// All results should have the same length as input
|
||||
expect(errorResult.length).toBe(text.length)
|
||||
expect(warnResult.length).toBe(text.length)
|
||||
expect(infoResult.length).toBe(text.length)
|
||||
expect(successResult.length).toBe(text.length)
|
||||
} catch (error) {
|
||||
console.error(`Failed with text: ${text}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("should handle edge cases", () => {
|
||||
const edgeCases = [
|
||||
"", // Empty string
|
||||
" ", // Single space
|
||||
" ", // Multiple spaces
|
||||
"0", // Zero as string
|
||||
"null", // "null" as string
|
||||
"undefined", // "undefined" as string
|
||||
"NaN", // "NaN" as string
|
||||
"true", // "true" as string
|
||||
"false", // "false" as string
|
||||
"[]", // Empty array as string
|
||||
"{}", // Empty object as string
|
||||
]
|
||||
|
||||
for (const text of edgeCases) {
|
||||
try {
|
||||
// Test each highlighter function
|
||||
const errorResult = highlighter.error(text)
|
||||
const warnResult = highlighter.warn(text)
|
||||
const infoResult = highlighter.info(text)
|
||||
const successResult = highlighter.success(text)
|
||||
|
||||
// All results should be strings
|
||||
expect(typeof errorResult).toBe("string")
|
||||
expect(typeof warnResult).toBe("string")
|
||||
expect(typeof infoResult).toBe("string")
|
||||
expect(typeof successResult).toBe("string")
|
||||
|
||||
// All results should have the same length as input
|
||||
expect(errorResult.length).toBe(text.length)
|
||||
expect(warnResult.length).toBe(text.length)
|
||||
expect(infoResult.length).toBe(text.length)
|
||||
expect(successResult.length).toBe(text.length)
|
||||
} catch (error) {
|
||||
console.error(`Failed with edge case: ${text}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -20,6 +20,8 @@ export async function updateCssVars(
|
||||
config: Config,
|
||||
options: {
|
||||
cleanupDefaultNextStyles?: boolean
|
||||
overwriteCssVars?: boolean
|
||||
initIndex?: boolean
|
||||
silent?: boolean
|
||||
tailwindVersion?: TailwindVersion
|
||||
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
@@ -33,6 +35,8 @@ export async function updateCssVars(
|
||||
cleanupDefaultNextStyles: false,
|
||||
silent: false,
|
||||
tailwindVersion: "v3",
|
||||
overwriteCssVars: false,
|
||||
initIndex: true,
|
||||
...options,
|
||||
}
|
||||
const cssFilepath = config.resolvedPaths.tailwindCss
|
||||
@@ -51,6 +55,8 @@ export async function updateCssVars(
|
||||
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
|
||||
tailwindVersion: options.tailwindVersion,
|
||||
tailwindConfig: options.tailwindConfig,
|
||||
overwriteCssVars: options.overwriteCssVars,
|
||||
initIndex: options.initIndex,
|
||||
})
|
||||
await fs.writeFile(cssFilepath, output, "utf8")
|
||||
cssVarsSpinner.succeed()
|
||||
@@ -64,16 +70,22 @@ export async function transformCssVars(
|
||||
cleanupDefaultNextStyles?: boolean
|
||||
tailwindVersion?: TailwindVersion
|
||||
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
overwriteCssVars?: boolean
|
||||
initIndex?: boolean
|
||||
} = {
|
||||
cleanupDefaultNextStyles: false,
|
||||
tailwindVersion: "v3",
|
||||
tailwindConfig: undefined,
|
||||
overwriteCssVars: false,
|
||||
initIndex: true,
|
||||
}
|
||||
) {
|
||||
options = {
|
||||
cleanupDefaultNextStyles: false,
|
||||
tailwindVersion: "v3",
|
||||
tailwindConfig: undefined,
|
||||
overwriteCssVars: false,
|
||||
initIndex: true,
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -91,7 +103,8 @@ export async function transformCssVars(
|
||||
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
|
||||
if (
|
||||
!packageInfo?.dependencies?.["tailwindcss-animate"] &&
|
||||
!packageInfo?.devDependencies?.["tailwindcss-animate"]
|
||||
!packageInfo?.devDependencies?.["tailwindcss-animate"] &&
|
||||
options.initIndex
|
||||
) {
|
||||
plugins.push(addCustomImport({ params: "tw-animate-css" }))
|
||||
}
|
||||
@@ -103,7 +116,11 @@ export async function transformCssVars(
|
||||
plugins.push(cleanupDefaultNextStylesPlugin())
|
||||
}
|
||||
|
||||
plugins.push(updateCssVarsPluginV4(cssVars))
|
||||
plugins.push(
|
||||
updateCssVarsPluginV4(cssVars, {
|
||||
overwriteCssVars: options.overwriteCssVars,
|
||||
})
|
||||
)
|
||||
plugins.push(updateThemePlugin(cssVars))
|
||||
|
||||
if (options.tailwindConfig) {
|
||||
@@ -113,7 +130,7 @@ export async function transformCssVars(
|
||||
}
|
||||
}
|
||||
|
||||
if (config.tailwind.cssVariables) {
|
||||
if (config.tailwind.cssVariables && options.initIndex) {
|
||||
plugins.push(
|
||||
updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion })
|
||||
)
|
||||
@@ -374,13 +391,51 @@ function addOrUpdateVars(
|
||||
}
|
||||
|
||||
function updateCssVarsPluginV4(
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>,
|
||||
options: {
|
||||
overwriteCssVars?: boolean
|
||||
}
|
||||
) {
|
||||
return {
|
||||
postcssPlugin: "update-css-vars-v4",
|
||||
Once(root: Root) {
|
||||
Object.entries(cssVars).forEach(([key, vars]) => {
|
||||
const selector = key === "light" ? ":root" : `.${key}`
|
||||
let selector = key === "light" ? ":root" : `.${key}`
|
||||
|
||||
if (key === "theme") {
|
||||
selector = "@theme"
|
||||
const themeNode = upsertThemeNode(root)
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
const prop = `--${key.replace(/^--/, "")}`
|
||||
const newDecl = postcss.decl({
|
||||
prop,
|
||||
value,
|
||||
raws: { semicolon: true },
|
||||
})
|
||||
|
||||
const existingDecl = themeNode?.nodes?.find(
|
||||
(node): node is postcss.Declaration =>
|
||||
node.type === "decl" && node.prop === prop
|
||||
)
|
||||
|
||||
// Only overwrite if overwriteCssVars is true
|
||||
// i.e for registry:theme and registry:style
|
||||
// We do not want new components to overwrite existing vars.
|
||||
// Keep user defined vars.
|
||||
if (options.overwriteCssVars) {
|
||||
if (existingDecl) {
|
||||
existingDecl.replaceWith(newDecl)
|
||||
} else {
|
||||
themeNode?.append(newDecl)
|
||||
}
|
||||
} else {
|
||||
if (!existingDecl) {
|
||||
themeNode?.append(newDecl)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let ruleNode = root.nodes?.find(
|
||||
(node): node is Rule =>
|
||||
@@ -419,11 +474,20 @@ function updateCssVarsPluginV4(
|
||||
node.type === "decl" && node.prop === prop
|
||||
)
|
||||
|
||||
// Do not override existing declarations.
|
||||
// We do not want new components to override existing vars.
|
||||
// Only overwrite if overwriteCssVars is true
|
||||
// i.e for registry:theme and registry:style
|
||||
// We do not want new components to overwrite existing vars.
|
||||
// Keep user defined vars.
|
||||
if (!existingDecl) {
|
||||
ruleNode?.append(newDecl)
|
||||
if (options.overwriteCssVars) {
|
||||
if (existingDecl) {
|
||||
existingDecl.replaceWith(newDecl)
|
||||
} else {
|
||||
ruleNode?.append(newDecl)
|
||||
}
|
||||
} else {
|
||||
if (!existingDecl) {
|
||||
ruleNode?.append(newDecl)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { registryItemTailwindSchema } from "@/src/registry/schema"
|
||||
import {
|
||||
registryItemCssVarsSchema,
|
||||
registryItemTailwindSchema,
|
||||
} from "@/src/registry/schema"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { TailwindVersion } from "@/src/utils/get-project-info"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
@@ -499,7 +502,7 @@ function parseValue(node: any): any {
|
||||
}
|
||||
|
||||
export function buildTailwindThemeColorsFromCssVars(
|
||||
cssVars: Record<string, string>
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>
|
||||
) {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
|
||||
16
packages/shadcn/test/fuzz-utils.ts
Normal file
16
packages/shadcn/test/fuzz-utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const generateRandomString = (length: number): string => {
|
||||
const chars =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
return Array.from(
|
||||
{ length },
|
||||
() => chars[Math.floor(Math.random() * chars.length)]
|
||||
).join("")
|
||||
}
|
||||
|
||||
export const generateRandomPath = (): string => {
|
||||
const segments = Array.from(
|
||||
{ length: Math.floor(Math.random() * 5) + 1 },
|
||||
() => generateRandomString(Math.floor(Math.random() * 10) + 1)
|
||||
)
|
||||
return segments.join("/")
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "path"
|
||||
import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths"
|
||||
import { expect, test } from "vitest"
|
||||
import { describe, expect, test } from "vitest"
|
||||
|
||||
import { resolveImport } from "../../src/utils/resolve-import"
|
||||
import { generateRandomPath } from "../fuzz-utils"
|
||||
|
||||
test("resolve import", async () => {
|
||||
expect(
|
||||
@@ -79,3 +80,88 @@ test("resolve import without base url", async () => {
|
||||
path.resolve(cwd, "foo/bar")
|
||||
)
|
||||
})
|
||||
|
||||
describe("fuzzing", () => {
|
||||
test("should handle various import paths", async () => {
|
||||
const generateRandomConfig = (): Pick<
|
||||
ConfigLoaderSuccessResult,
|
||||
"absoluteBaseUrl" | "paths"
|
||||
> => ({
|
||||
absoluteBaseUrl: generateRandomPath(),
|
||||
paths: {
|
||||
"@/*": [generateRandomPath()],
|
||||
"@/components/*": [generateRandomPath()],
|
||||
"@/lib/*": [generateRandomPath()],
|
||||
},
|
||||
})
|
||||
|
||||
const testCases = Array.from({ length: 100 }, () => ({
|
||||
importPath: generateRandomPath(),
|
||||
config: generateRandomConfig(),
|
||||
}))
|
||||
|
||||
for (const { importPath, config } of testCases) {
|
||||
try {
|
||||
const result = await resolveImport(importPath, config)
|
||||
// Should either return undefined or a valid path
|
||||
if (result) {
|
||||
expect(typeof result).toBe("string")
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
}
|
||||
} catch (error) {
|
||||
// Expected for invalid paths
|
||||
expect(error).toBeDefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("should handle edge cases", async () => {
|
||||
const edgeCases: Array<{
|
||||
importPath: string
|
||||
config: Pick<ConfigLoaderSuccessResult, "absoluteBaseUrl" | "paths">
|
||||
}> = [
|
||||
{
|
||||
importPath: "",
|
||||
config: {
|
||||
absoluteBaseUrl: "",
|
||||
paths: {
|
||||
"@/*": [""],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
importPath: "/",
|
||||
config: {
|
||||
absoluteBaseUrl: "/",
|
||||
paths: {
|
||||
"@/*": ["/"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
importPath: "@/components/button",
|
||||
config: {
|
||||
absoluteBaseUrl: "/",
|
||||
paths: {
|
||||
"@/*": ["/"],
|
||||
"@/components/*": ["/components"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for (const { importPath, config } of edgeCases) {
|
||||
try {
|
||||
const result = await resolveImport(importPath, config)
|
||||
// Should either return undefined or a valid path
|
||||
if (result) {
|
||||
expect(typeof result).toBe("string")
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed with edge case:`, { importPath, config }, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ exports[`registryResolveItemTree > should resolve index 1`] = `
|
||||
"light": {
|
||||
"radius": "0.5rem",
|
||||
},
|
||||
"theme": {},
|
||||
},
|
||||
"dependencies": [
|
||||
"tailwindcss-animate",
|
||||
@@ -166,7 +167,7 @@ exports[`registryResolveItemTree > should resolve multiple items tree 1`] = `
|
||||
"cssVars": {},
|
||||
"dependencies": [
|
||||
"@radix-ui/react-slot",
|
||||
"cmdk@1.0.0",
|
||||
"cmdk",
|
||||
"@radix-ui/react-dialog",
|
||||
],
|
||||
"devDependencies": [],
|
||||
|
||||
@@ -309,6 +309,12 @@ describe("transformCssVarsV4", () => {
|
||||
}
|
||||
`,
|
||||
{
|
||||
theme: {
|
||||
"font-poppins": "Poppins, sans-serif",
|
||||
"breakpoint-3xl": "120rem",
|
||||
"shadow-2xs": "0px 1px 2px 0px rgba(0, 0, 0, 0.05)",
|
||||
"animate-bounce": "bounce 1s infinite",
|
||||
},
|
||||
light: {
|
||||
background: "215 20.2% 65.1%",
|
||||
foreground: "222.2 84% 4.9%",
|
||||
@@ -340,6 +346,152 @@ describe("transformCssVarsV4", () => {
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--font-poppins: Poppins, sans-serif;
|
||||
--breakpoint-3xl: 120rem;
|
||||
--shadow-2xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
|
||||
--animate-bounce: bounce 1s infinite;
|
||||
--color-primary: var(--primary);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should NOT override theme vars if overwriteCssVars is false", async () => {
|
||||
expect(
|
||||
await transformCssVars(
|
||||
`@import "tailwindcss";
|
||||
:root {
|
||||
--background: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--font-sans: Inter, sans-serif;
|
||||
}
|
||||
`,
|
||||
{
|
||||
theme: {
|
||||
"font-sans": "Poppins, sans-serif",
|
||||
"breakpoint-3xl": "120rem",
|
||||
},
|
||||
light: {
|
||||
background: "215 20.2% 65.1%",
|
||||
foreground: "222.2 84% 4.9%",
|
||||
primary: "215 20.2% 65.1%",
|
||||
},
|
||||
dark: {
|
||||
foreground: "60 9.1% 97.8%",
|
||||
primary: "222.2 84% 4.9%",
|
||||
},
|
||||
},
|
||||
{ tailwind: { cssVariables: true } },
|
||||
{ tailwindVersion: "v4" }
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
:root {
|
||||
--background: hsl(210 40% 98%);
|
||||
--foreground: hsl(222.2 84% 4.9%);
|
||||
--primary: hsl(215 20.2% 65.1%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(222.2 84% 4.9%);
|
||||
--foreground: hsl(60 9.1% 97.8%);
|
||||
--primary: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--font-sans: Inter, sans-serif;
|
||||
--breakpoint-3xl: 120rem;
|
||||
--color-primary: var(--primary);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should override theme vars if overwriteCssVars is true", async () => {
|
||||
expect(
|
||||
await transformCssVars(
|
||||
`@import "tailwindcss";
|
||||
:root {
|
||||
--background: hsl(210 40% 98%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--font-sans: Inter, sans-serif;
|
||||
}
|
||||
`,
|
||||
{
|
||||
theme: {
|
||||
"font-sans": "Poppins, sans-serif",
|
||||
"breakpoint-3xl": "120rem",
|
||||
},
|
||||
light: {
|
||||
background: "215 20.2% 65.1%",
|
||||
foreground: "222.2 84% 4.9%",
|
||||
primary: "215 20.2% 65.1%",
|
||||
},
|
||||
dark: {
|
||||
foreground: "60 9.1% 97.8%",
|
||||
primary: "222.2 84% 4.9%",
|
||||
},
|
||||
},
|
||||
{ tailwind: { cssVariables: true } },
|
||||
{ tailwindVersion: "v4", overwriteCssVars: true }
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
:root {
|
||||
--background: hsl(215 20.2% 65.1%);
|
||||
--foreground: hsl(222.2 84% 4.9%);
|
||||
--primary: hsl(215 20.2% 65.1%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(222.2 84% 4.9%);
|
||||
--foreground: hsl(60 9.1% 97.8%);
|
||||
--primary: hsl(222.2 84% 4.9%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--font-sans: Poppins, sans-serif;
|
||||
--breakpoint-3xl: 120rem;
|
||||
--color-primary: var(--primary);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user