Compare commits

..

1 Commits

Author SHA1 Message Date
shadcn
dcd08c1abe feat(shadcn): remove npm flags 2025-03-24 09:02:00 +04:00
30 changed files with 83 additions and 708 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": minor
---
add theme prop to registry-item schema

View File

@@ -240,7 +240,7 @@
"name": "command",
"type": "registry:ui",
"dependencies": [
"cmdk"
"cmdk@1.0.0"
],
"registryDependencies": [
"dialog"

View File

@@ -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/60",
"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",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},

View File

@@ -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 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] 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 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"
)
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 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",
"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",
className
)}
{...props}

View File

@@ -18,7 +18,7 @@ function ScrollArea({
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
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"
>
{children}
</ScrollAreaPrimitive.Viewport>

View File

@@ -49,46 +49,6 @@ 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}
@@ -130,47 +90,9 @@ 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`.
</TabsContent>
</Tabs>
### How do I add a Tailwind animation?
### 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.
To add a new animation you add it to `tailwind.config.theme.extend.animation` and `tailwind.config.theme.extend.keyframes`.
```json showLineNumbers {14-22}
{
@@ -186,8 +108,14 @@ To override a theme variable you add it to `tailwind.config.theme.extend` under
"config": {
"theme": {
"extend": {
"text": {
"base": "3rem"
"keyframes": {
"wiggle": {
"0%, 100%": { "transform": "rotate(-3deg)" },
"50%": { "transform": "rotate(3deg)" }
}
},
"animation": {
"wiggle": "wiggle 1s ease-in-out infinite"
}
}
}
@@ -195,6 +123,3 @@ To override a theme variable you add it to `tailwind.config.theme.extend` under
}
}
```
</TabsContent>
</Tabs>

View File

@@ -21,18 +21,7 @@ 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%"
}
}
]
}
```
@@ -52,7 +41,7 @@ The `$schema` property is used to specify the schema for the `registry-item.json
### name
The name of the item. This is used to identify the item in the registry. It should be unique for your registry.
The `name` property is used to specify the name of your registry item.
```json title="registry-item.json" showLineNumbers
{
@@ -82,7 +71,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. This is used to determine the type and target path of the item when resolved for a project.
The `type` property is used to specify the type of your registry item.
```json title="registry-item.json" showLineNumbers
{
@@ -101,8 +90,6 @@ 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
@@ -135,7 +122,7 @@ Use `@version` to specify the version of your registry item.
### registryDependencies
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.
Used for registry dependencies. Can be names or URLs.
- 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']`.
@@ -202,8 +189,6 @@ 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.
@@ -240,9 +225,6 @@ 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"
@@ -254,6 +236,11 @@ 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.

View File

@@ -92,7 +92,6 @@ 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);
@@ -108,6 +107,7 @@ 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,6 +116,7 @@ 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);
@@ -129,21 +130,22 @@ 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.205 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.269 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary: oklch(0.985 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.371 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--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);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
@@ -156,7 +158,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(1 0 0 / 10%);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
```

View File

@@ -223,7 +223,7 @@
"name": "command",
"type": "registry:ui",
"dependencies": [
"cmdk"
"cmdk@1.0.0"
],
"registryDependencies": [
"dialog"

View File

@@ -4,7 +4,7 @@
"type": "registry:ui",
"author": "shadcn (https://ui.shadcn.com)",
"dependencies": [
"cmdk"
"cmdk@1.0.0"
],
"registryDependencies": [
"dialog"

View File

@@ -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/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",
"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",
"type": "registry:ui"
}
]

View File

@@ -3,7 +3,7 @@
"name": "command",
"type": "registry:ui",
"dependencies": [
"cmdk"
"cmdk@1.0.0"
],
"registryDependencies": [
"dialog"

File diff suppressed because one or more lines are too long

View File

@@ -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=\"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",
"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",
"type": "registry:ui"
}
]

View File

@@ -4,7 +4,7 @@
"type": "registry:ui",
"author": "shadcn (https://ui.shadcn.com)",
"dependencies": [
"cmdk"
"cmdk@1.0.0"
],
"registryDependencies": [
"dialog"

View File

@@ -3,8 +3,7 @@
"type": "object",
"properties": {
"name": {
"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": "string"
},
"type": {
"type": "string",
@@ -16,57 +15,46 @@
"registry:hook",
"registry:theme",
"registry:page",
"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."
"registry:file"
]
},
"description": {
"type": "string",
"description": "The description of the item. This is used to provide a brief overview of the item."
"type": "string"
},
"title": {
"type": "string",
"description": "The human-readable title for your registry item. Keep it short and descriptive."
"type": "string"
},
"author": {
"type": "string",
"description": "The author of the item. Recommended format: username <url>"
"type": "string"
},
"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",
"description": "The path to the file relative to the registry root."
"type": "string"
},
"content": {
"type": "string",
"description": "The content of the file."
"type": "string"
},
"type": {
"type": "string",
@@ -79,12 +67,10 @@
"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",
"description": "The target path of the file. This is the path to the file in the project."
"type": "string"
}
},
"if": {
@@ -104,7 +90,6 @@
},
"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",
@@ -131,25 +116,15 @@
},
"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"
}
@@ -158,23 +133,16 @@
},
"meta": {
"type": "object",
"description": "Additional metadata for the registry item. This is an object with any key value pairs.",
"additionalProperties": true
},
"docs": {
"type": "string",
"description": "The documentation for the registry item. This is a markdown string."
"type": "string"
},
"categories": {
"type": "array",
"items": {
"type": "string",
"description": "The categories of the registry item. This is an array of strings."
"type": "string"
}
},
"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"]

View File

@@ -182,7 +182,7 @@ export const ui: Registry["items"] = [
{
name: "command",
type: "registry:ui",
dependencies: ["cmdk"],
dependencies: ["cmdk@1.0.0"],
registryDependencies: ["dialog"],
files: [
{

View File

@@ -148,7 +148,6 @@ export const add = new Command()
isNewProject: false,
srcDir: options.srcDir,
cssVariables: options.cssVariables,
style: "index",
})
}
@@ -180,7 +179,6 @@ export const add = new Command()
isNewProject: true,
srcDir: options.srcDir,
cssVariables: options.cssVariables,
style: "index",
})
shouldUpdateAppIndex =

View File

@@ -4,7 +4,6 @@ import { preFlightInit } from "@/src/preflights/preflight-init"
import {
BASE_COLORS,
getRegistryBaseColors,
getRegistryItem,
getRegistryStyles,
} from "@/src/registry/api"
import { addComponents } from "@/src/utils/add-components"
@@ -75,7 +74,6 @@ export const initOptionsSchema = z.object({
).join("', '")}'`,
}
),
style: z.string(),
})
export const init = new Command()
@@ -120,22 +118,9 @@ 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(
@@ -206,15 +191,11 @@ export async function runInit(
// Add components.
const fullConfig = await resolveConfigPaths(options.cwd, config)
const components = [
...(options.style === "none" ? [] : [options.style]),
...(options.components ?? []),
]
const components = ["index", ...(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",
})

View File

@@ -289,14 +289,6 @@ 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 ?? {})
@@ -404,7 +396,6 @@ export async function registryGetTheme(name: string, config: Config) {
},
},
cssVars: {
theme: {},
light: {
radius: "0.5rem",
},
@@ -415,13 +406,9 @@ 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,
@@ -434,10 +421,6 @@ 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,

View File

@@ -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,14 +46,12 @@ 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(),
@@ -99,8 +97,16 @@ export const registryBaseColorSchema = z.object({
light: z.record(z.string(), z.string()),
dark: z.record(z.string(), z.string()),
}),
cssVars: registryItemCssVarsSchema,
cssVarsV4: registryItemCssVarsSchema.optional(),
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(),
inlineColorsTemplate: z.string(),
cssVarsTemplate: z.string(),
})

View File

@@ -32,14 +32,12 @@ export async function addComponents(
overwrite?: boolean
silent?: boolean
isNewProject?: boolean
style?: string
}
) {
options = {
overwrite: false,
silent: false,
isNewProject: false,
style: "index",
...options,
}
@@ -66,14 +64,12 @@ 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."))
@@ -86,15 +82,11 @@ 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, {
@@ -119,7 +111,6 @@ async function addWorkspaceComponents(
silent?: boolean
isNewProject?: boolean
isRemote?: boolean
style?: string
}
) {
const registrySpinner = spinner(`Checking registry.`, {
@@ -184,12 +175,10 @@ 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)
@@ -282,17 +271,3 @@ 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"
)
}

View File

@@ -1,78 +0,0 @@
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
}
}
})
})

View File

@@ -20,8 +20,6 @@ export async function updateCssVars(
config: Config,
options: {
cleanupDefaultNextStyles?: boolean
overwriteCssVars?: boolean
initIndex?: boolean
silent?: boolean
tailwindVersion?: TailwindVersion
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
@@ -35,8 +33,6 @@ export async function updateCssVars(
cleanupDefaultNextStyles: false,
silent: false,
tailwindVersion: "v3",
overwriteCssVars: false,
initIndex: true,
...options,
}
const cssFilepath = config.resolvedPaths.tailwindCss
@@ -55,8 +51,6 @@ 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()
@@ -70,22 +64,16 @@ 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,
}
@@ -103,8 +91,7 @@ export async function transformCssVars(
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
if (
!packageInfo?.dependencies?.["tailwindcss-animate"] &&
!packageInfo?.devDependencies?.["tailwindcss-animate"] &&
options.initIndex
!packageInfo?.devDependencies?.["tailwindcss-animate"]
) {
plugins.push(addCustomImport({ params: "tw-animate-css" }))
}
@@ -116,11 +103,7 @@ export async function transformCssVars(
plugins.push(cleanupDefaultNextStylesPlugin())
}
plugins.push(
updateCssVarsPluginV4(cssVars, {
overwriteCssVars: options.overwriteCssVars,
})
)
plugins.push(updateCssVarsPluginV4(cssVars))
plugins.push(updateThemePlugin(cssVars))
if (options.tailwindConfig) {
@@ -130,7 +113,7 @@ export async function transformCssVars(
}
}
if (config.tailwind.cssVariables && options.initIndex) {
if (config.tailwind.cssVariables) {
plugins.push(
updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion })
)
@@ -391,51 +374,13 @@ function addOrUpdateVars(
}
function updateCssVarsPluginV4(
cssVars: z.infer<typeof registryItemCssVarsSchema>,
options: {
overwriteCssVars?: boolean
}
cssVars: z.infer<typeof registryItemCssVarsSchema>
) {
return {
postcssPlugin: "update-css-vars-v4",
Once(root: Root) {
Object.entries(cssVars).forEach(([key, vars]) => {
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
}
const selector = key === "light" ? ":root" : `.${key}`
let ruleNode = root.nodes?.find(
(node): node is Rule =>
@@ -474,20 +419,11 @@ function updateCssVarsPluginV4(
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.
// Do not override existing declarations.
// We do not want new components to override existing vars.
// Keep user defined vars.
if (options.overwriteCssVars) {
if (existingDecl) {
existingDecl.replaceWith(newDecl)
} else {
ruleNode?.append(newDecl)
}
} else {
if (!existingDecl) {
ruleNode?.append(newDecl)
}
if (!existingDecl) {
ruleNode?.append(newDecl)
}
})
})

View File

@@ -1,11 +1,8 @@
import { RegistryItem } from "@/src/registry/schema"
import { Config } from "@/src/utils/get-config"
import { getPackageInfo } from "@/src/utils/get-package-info"
import { getPackageManager } from "@/src/utils/get-package-manager"
import { logger } from "@/src/utils/logger"
import { spinner } from "@/src/utils/spinner"
import { execa } from "execa"
import prompts from "prompts"
export async function updateDependencies(
dependencies: RegistryItem["dependencies"],
@@ -29,43 +26,11 @@ export async function updateDependencies(
})?.start()
const packageManager = await getPackageManager(config.resolvedPaths.cwd)
// Offer to use --force or --legacy-peer-deps if using React 19 with npm.
let flag = ""
if (isUsingReact19(config) && packageManager === "npm") {
if (options.silent) {
flag = "force"
} else {
dependenciesSpinner.stopAndPersist()
logger.warn(
"\nIt looks like you are using React 19. \nSome packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).\n"
)
const confirmation = await prompts([
{
type: "select",
name: "flag",
message: "How would you like to proceed?",
choices: [
{ title: "Use --force", value: "force" },
{ title: "Use --legacy-peer-deps", value: "legacy-peer-deps" },
],
},
])
if (confirmation) {
flag = confirmation.flag
}
}
}
dependenciesSpinner?.start()
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
...dependencies,
],
[packageManager === "npm" ? "install" : "add", ...dependencies],
{
cwd: config.resolvedPaths.cwd,
}
@@ -73,13 +38,3 @@ export async function updateDependencies(
dependenciesSpinner?.succeed()
}
function isUsingReact19(config: Config) {
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
if (!packageInfo?.dependencies?.react) {
return false
}
return /^(?:\^|~)?19(?:\.\d+)*(?:-.*)?$/.test(packageInfo.dependencies.react)
}

View File

@@ -1,10 +1,7 @@
import { promises as fs } from "fs"
import { tmpdir } from "os"
import path from "path"
import {
registryItemCssVarsSchema,
registryItemTailwindSchema,
} from "@/src/registry/schema"
import { 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"
@@ -502,7 +499,7 @@ function parseValue(node: any): any {
}
export function buildTailwindThemeColorsFromCssVars(
cssVars: z.infer<typeof registryItemCssVarsSchema>
cssVars: Record<string, string>
) {
const result: Record<string, any> = {}

View File

@@ -1,16 +0,0 @@
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("/")
}

View File

@@ -1,9 +1,8 @@
import path from "path"
import { loadConfig, type ConfigLoaderSuccessResult } from "tsconfig-paths"
import { describe, expect, test } from "vitest"
import { expect, test } from "vitest"
import { resolveImport } from "../../src/utils/resolve-import"
import { generateRandomPath } from "../fuzz-utils"
test("resolve import", async () => {
expect(
@@ -80,88 +79,3 @@ 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
}
}
})
})

View File

@@ -7,7 +7,6 @@ exports[`registryResolveItemTree > should resolve index 1`] = `
"light": {
"radius": "0.5rem",
},
"theme": {},
},
"dependencies": [
"tailwindcss-animate",
@@ -167,7 +166,7 @@ exports[`registryResolveItemTree > should resolve multiple items tree 1`] = `
"cssVars": {},
"dependencies": [
"@radix-ui/react-slot",
"cmdk",
"cmdk@1.0.0",
"@radix-ui/react-dialog",
],
"devDependencies": [],

View File

@@ -309,12 +309,6 @@ 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%",
@@ -346,152 +340,6 @@ 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);
}