Compare commits

..

16 Commits

Author SHA1 Message Date
github-actions[bot]
84d6c83bad chore(release): version packages (#7626)
* chore(release): version packages

* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-18 16:01:20 +04:00
xabierlameiro.com
5b8ee41511 fix(cli): correct function name typo unnsetSpreadElements to unsetSpreadElements (#7609)
* fix(cli): correct function name typo unnsetSpreadElements to unsetSpreadElements

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-18 11:44:59 +04:00
shadcn
7c3d34cdc9 chore: fix changeset (#7640)
* fix(shadcn): update plugin handling

* style(shadcn): format fix

* docs(www): add docs for plugins

* chore: add changeset
2025-06-18 11:29:23 +04:00
shadcn
56c4c83511 fix(shadcn): update plugin handling (#7632)
* fix(shadcn): update plugin handling

* style(shadcn): format fix

* docs(www): add docs for plugins
2025-06-18 11:03:36 +04:00
shadcn
2821cb0e39 chore: move cli to deprecated (#7631) 2025-06-17 12:32:10 +04:00
Wolfr
3c87402de2 Add newly available Figma kit to docs (#7604)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 16:21:26 +04:00
xabierlameiro.com
20a88e1f15 fix(components): resolve duplicate id conflict in calendar-24 component (#7611)
* fix(components): resolve duplicate id conflict in calendar-24 component

- Changed Button id from 'date' to 'date-picker'
- Changed Input id from 'time' to 'time-picker'
- Updated corresponding Label htmlFor attributes to match new unique IDs

Fixes #7561

* chore: rebuild registry after calendar-24 fixes
2025-06-16 16:13:51 +04:00
Zach Nugent
cb19ab8464 feat(shadcn): add support for updating dependencies with expo-cli for RN compatibility (#7540)
* feat(shadcn): add support for updating dependencies with expo-cli for RN compatibility

* feat(shadcn): add expo as a framework

* fix: update the contributing command for registry

* refactor(shadcn): update dependencies install functionality

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 16:05:41 +04:00
github-actions[bot]
cf1851ca09 chore(release): version packages (#7625)
* chore(release): version packages

* deps: update lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:45:23 +04:00
Manuel Schiller
c86c27a2ff fix TanStack Start detection (#7601)
* fix tanstack start detection

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:24:41 +04:00
Gaëtan H
8847126c65 chore(vscode): set custom Tailwind config path for monorepo UI (#7618)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-16 15:18:27 +04:00
shadcn
65350857a4 ci: fix stale bot (#7624) 2025-06-16 15:02:53 +04:00
shadcn
40c7473c7e fix(www): update open-in-v0-cta.tsx 2025-06-14 06:20:16 +04:00
Taesu
4698ee960f chore: update react-day-picker version to match updated calendar component (#7585)
Co-authored-by: shadcn <m@shadcn.com>
2025-06-12 15:44:40 +04:00
github-actions[bot]
2ae0e5a07b chore(release): version packages (#7595)
* chore(release): version packages

* deps: install

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-06-11 20:06:18 +04:00
shadcn
431af4f7ff fix(shadcn): semicolon in code style (#7594)
* fix(shadcn): handle semicolon in code style

* chore: changeset

* fix: format
2025-06-11 19:54:04 +04:00
40 changed files with 640 additions and 167 deletions

View File

@@ -18,15 +18,15 @@ jobs:
repo-token: ${{ secrets.STALE_TOKEN }}
ascending: true
days-before-issue-close: 7
days-before-issue-stale: 365 # ~2 years
days-before-issue-stale: 365
days-before-pr-stale: -1
days-before-pr-close: -1
remove-issue-stale-when-updated: true
stale-issue-label: "stale?"
exempt-issue-labels: "roadmap,next,bug"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you."
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
exempt-issue-labels: "roadmap,next"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300
- uses: actions/stale@v9
id: pr-state
name: "Mark stale PRs, close stale PRs"
@@ -36,10 +36,10 @@ jobs:
days-before-issue-close: -1
days-before-issue-stale: -1
days-before-pr-close: 7
days-before-pr-stale: 365 # PRs with no activity in over 90 days will be marked as stale
days-before-pr-stale: 365
remove-pr-stale-when-updated: true
exempt-pr-labels: "roadmap,nex,awaiting-approval,work-in-progress"
exempt-pr-labels: "roadmap,next,bug"
stale-pr-label: "stale?"
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you."
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding!"
operations-per-run: 300 # 1 operation per 100 issues, the rest is to label/comment/close
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300

View File

@@ -98,7 +98,7 @@ To run the CLI locally, you can follow the workflow:
1. Start by running the registry (main site) to make sure the components are up to date:
```bash
pnpm www:dev
pnpm v4:dev
```
2. Run the development script for the CLI:

2
apps/v4/.env.example Normal file
View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_V0_URL=https://v0.dev
NEXT_PUBLIC_APP_URL=http://localhost:4000

1
apps/v4/.gitignore vendored
View File

@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel

View File

@@ -13,7 +13,7 @@ export function OpenInV0Cta({ className }: React.ComponentProps<"div">) {
Deploy your shadcn/ui app on Vercel
</div>
<div className="text-muted-foreground">
Trusted by OpenAI, Sonos, Chick-fil-A, and more.
Trusted by OpenAI, Sonos, Adobe, and more.
</div>
<div className="text-muted-foreground">
Vercel provides tools and infrastructure to deploy apps and features at

View File

@@ -41,7 +41,7 @@ npx shadcn@latest add calendar
<Step>Install the following dependencies:</Step>
```bash
npm install react-day-picker@8.10.1 date-fns
npm install react-day-picker date-fns
```
<Step>Add the `Button` component to your project.</Step>

View File

@@ -328,6 +328,20 @@ Add custom theme variables to the `theme` object.
}
```
## Add custom plugins
```json title="example-plugin.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "custom-plugin",
"type": "registry:component",
"css": {
"@plugin @tailwindcss/typography": {},
"@plugin foo": {}
}
}
```
## Add custom animations
Note: you need to define both `@keyframes` in css and `theme` in cssVars to use animations.

View File

@@ -260,11 +260,13 @@ Use to define CSS variables for your registry item.
### css
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, etc.
Use `css` to add new rules to the project's CSS file eg. `@layer base`, `@layer components`, `@utility`, `@keyframes`, `@plugin`, etc.
```json title="registry-item.json" showLineNumbers
{
"css": {
"@plugin @tailwindcss/typography": {},
"@plugin foo": {},
"@layer base": {
"body": {
"font-size": "var(--text-base)",

View File

@@ -86,7 +86,7 @@
"recharts": "2.15.1",
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"shadcn": "2.6.2",
"shadcn": "2.7.0",
"shiki": "^1.10.1",
"sonner": "^2.0.0",
"tailwind-merge": "^3.0.1",

View File

@@ -12,7 +12,7 @@
"files": [
{
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
"type": "registry:component"
}
],

View File

@@ -20,14 +20,14 @@ export default function Calendar24() {
return (
<div className="flex gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="date" className="px-1">
<Label htmlFor="date-picker" className="px-1">
Date
</Label>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
id="date-picker"
className="w-32 justify-between font-normal"
>
{date ? date.toLocaleDateString() : "Select date"}
@@ -48,12 +48,12 @@ export default function Calendar24() {
</Popover>
</div>
<div className="flex flex-col gap-3">
<Label htmlFor="time" className="px-1">
<Label htmlFor="time-picker" className="px-1">
Time
</Label>
<Input
type="time"
id="time"
id="time-picker"
step="1"
defaultValue="10:30:00"
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"

6
apps/www/README.md Normal file
View File

@@ -0,0 +1,6 @@
# ui.shadcn.com
> [!WARNING]
> The site at ui.shadcn.com now points to `apps/v4`. All changes should be made in the `apps/v4` directory.
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).

View File

@@ -39,7 +39,7 @@ npx shadcn@latest add calendar
<Step>Install the following dependencies:</Step>
```bash
npm install react-day-picker@8.10.1 date-fns
npm install react-day-picker date-fns
```
<Step>Add the `Button` component to your project.</Step>

View File

@@ -10,3 +10,4 @@ description: Every component recreated in Figma. With customizable props, typogr
## Free
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed

View File

@@ -88,7 +88,7 @@
"react-resizable-panels": "^2.0.22",
"react-wrap-balancer": "^0.4.1",
"recharts": "2.12.7",
"shadcn": "2.6.2",
"shadcn": "2.7.0",
"sharp": "^0.32.6",
"sonner": "^1.2.3",
"swr": "2.2.6-beta.3",

View File

@@ -12,7 +12,7 @@
"files": [
{
"path": "registry/new-york-v4/blocks/calendar-24.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { Button } from \"@/registry/new-york-v4/ui/button\"\nimport { Calendar } from \"@/registry/new-york-v4/ui/calendar\"\nimport { Input } from \"@/registry/new-york-v4/ui/input\"\nimport { Label } from \"@/registry/new-york-v4/ui/label\"\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/registry/new-york-v4/ui/popover\"\n\nexport default function Calendar24() {\n const [open, setOpen] = React.useState(false)\n const [date, setDate] = React.useState<Date | undefined>(undefined)\n\n return (\n <div className=\"flex gap-4\">\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"date-picker\" className=\"px-1\">\n Date\n </Label>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n variant=\"outline\"\n id=\"date-picker\"\n className=\"w-32 justify-between font-normal\"\n >\n {date ? date.toLocaleDateString() : \"Select date\"}\n <ChevronDownIcon />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto overflow-hidden p-0\" align=\"start\">\n <Calendar\n mode=\"single\"\n selected={date}\n captionLayout=\"dropdown\"\n onSelect={(date) => {\n setDate(date)\n setOpen(false)\n }}\n />\n </PopoverContent>\n </Popover>\n </div>\n <div className=\"flex flex-col gap-3\">\n <Label htmlFor=\"time-picker\" className=\"px-1\">\n Time\n </Label>\n <Input\n type=\"time\"\n id=\"time-picker\"\n step=\"1\"\n defaultValue=\"10:30:00\"\n className=\"bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none\"\n />\n </div>\n </div>\n )\n}\n",
"type": "registry:component"
}
],

View File

@@ -79,7 +79,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,7 +1,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,7 +1,7 @@
{
"name": "calendar",
"dependencies": [
"react-day-picker@8.10.1",
"react-day-picker",
"date-fns"
],
"registryDependencies": [

View File

@@ -1,5 +1,29 @@
# @shadcn/ui
## 2.7.0
### Minor Changes
- [#7540](https://github.com/shadcn-ui/ui/pull/7540) [`cb19ab84646fc017c15fadc81fc47b695560a04c`](https://github.com/shadcn-ui/ui/commit/cb19ab84646fc017c15fadc81fc47b695560a04c) Thanks [@mrzachnugent](https://github.com/mrzachnugent)! - add support for expo
- [#7640](https://github.com/shadcn-ui/ui/pull/7640) [`7c3d34cdc91681815f8897709917ec9fbcd69245`](https://github.com/shadcn-ui/ui/commit/7c3d34cdc91681815f8897709917ec9fbcd69245) Thanks [@shadcn](https://github.com/shadcn)! - add support for @plugin in css
### Patch Changes
- [#7609](https://github.com/shadcn-ui/ui/pull/7609) [`5b8ee41511fb5ff468d9218f97b8545e145d773c`](https://github.com/shadcn-ui/ui/commit/5b8ee41511fb5ff468d9218f97b8545e145d773c) Thanks [@xabierlameiro](https://github.com/xabierlameiro)! - fix typo in function name unnsetSpreadElements
## 2.6.4
### Patch Changes
- [#7601](https://github.com/shadcn-ui/ui/pull/7601) [`c86c27a2ffb8d186770afa42bfb62ab46e3db975`](https://github.com/shadcn-ui/ui/commit/c86c27a2ffb8d186770afa42bfb62ab46e3db975) Thanks [@schiller-manuel](https://github.com/schiller-manuel)! - fix tanstack start detection
## 2.6.3
### Patch Changes
- [#7594](https://github.com/shadcn-ui/ui/pull/7594) [`431af4f7ff294af032c0687b8b655ed6db2e690f`](https://github.com/shadcn-ui/ui/commit/431af4f7ff294af032c0687b8b655ed6db2e690f) Thanks [@shadcn](https://github.com/shadcn)! - fix: semicolon in code style
## 2.6.2
### Patch Changes

View File

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

View File

@@ -88,7 +88,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui";
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui"
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
@@ -109,7 +109,7 @@ export const DialogRoot = Root
export const DialogTrigger = Trigger
export const SelectContent = Content`
const expected = `import { Root, Trigger, Content } from "radix-ui";
const expected = `import { Root, Trigger, Content } from "radix-ui"
export const DialogRoot = Root
export const DialogTrigger = Trigger
@@ -131,7 +131,7 @@ import { useState } from "react"
export const Dialog = DialogPrimitive.Root
export const Select = SelectRoot`
const expected = `import { Dialog as DialogPrimitive, Root as SelectRoot } from "radix-ui";
const expected = `import { Dialog as DialogPrimitive, Root as SelectRoot } from "radix-ui"
import { useState } from "react"
@@ -184,7 +184,7 @@ export const Dialog = DialogPrimitive.Root`
const expected = `"use client"
import { Dialog as DialogPrimitive } from "radix-ui";
import { Dialog as DialogPrimitive } from "radix-ui"
import { useState } from "react"
export const Dialog = DialogPrimitive.Root`
@@ -206,7 +206,7 @@ export const Dialog = DialogPrimitive.Root`
const expected = `"use client"
import { DropdownMenu as DropdownMenuPrimitive, Dialog as DialogPrimitive } from "radix-ui";
import { DropdownMenu as DropdownMenuPrimitive, Dialog as DialogPrimitive } from "radix-ui"
import { useState } from "react"
@@ -228,7 +228,7 @@ import * as SelectPrimitive from '@radix-ui/react-select'
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui';
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui'
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
@@ -248,7 +248,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui';
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from 'radix-ui'
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
@@ -270,7 +270,7 @@ export type MyDialogProps = ComponentProps
export type MySelectProps = SelectProps
export const Dialog = DialogPrimitive.Root`
const expected = `import { type ComponentProps, type SelectProps, Root, Dialog as DialogPrimitive } from "radix-ui";
const expected = `import { type ComponentProps, type SelectProps, Root, Dialog as DialogPrimitive } from "radix-ui"
export type MyDialogProps = ComponentProps
export type MySelectProps = SelectProps
@@ -291,7 +291,7 @@ import { Root, Trigger } from "@radix-ui/react-dialog"
export type Props = DialogProps
export const DialogRoot = Root`
const expected = `import { type DialogProps, Root, Trigger } from "radix-ui";
const expected = `import { type DialogProps, Root, Trigger } from "radix-ui"
export type Props = DialogProps
export const DialogRoot = Root`
@@ -308,7 +308,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
export type Props = DialogTypes.ComponentProps
export const Dialog = DialogPrimitive.Root`
const expected = `import { type Dialog as DialogTypes, Dialog as DialogPrimitive } from "radix-ui";
const expected = `import { type Dialog as DialogTypes, Dialog as DialogPrimitive } from "radix-ui"
export type Props = DialogTypes.ComponentProps
export const Dialog = DialogPrimitive.Root`
@@ -327,7 +327,7 @@ export const Dialog = DialogPrimitive.Root
export const ChevronDown = ChevronDownIcon`
const expected = `import { ChevronDownIcon, Cross2Icon } from "@radix-ui/react-icons"
import { Dialog as DialogPrimitive, Root } from "radix-ui";
import { Dialog as DialogPrimitive, Root } from "radix-ui"
export const Dialog = DialogPrimitive.Root
export const ChevronDown = ChevronDownIcon`
@@ -349,7 +349,7 @@ export type MyIconProps = IconProps
export type MyDialogProps = ComponentProps`
const expected = `import type { IconProps } from "@radix-ui/react-icons/dist/types"
import { type ComponentProps, Root } from "radix-ui";
import { type ComponentProps, Root } from "radix-ui"
export type MyIconProps = IconProps
export type MyDialogProps = ComponentProps`
@@ -373,7 +373,7 @@ export type Props = IconProps`
const expected = `import * as Icons from "@radix-ui/react-icons"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import type { IconProps } from "@radix-ui/react-icons/dist/types"
import { Dialog as DialogPrimitive, Root } from "radix-ui";
import { Dialog as DialogPrimitive, Root } from "radix-ui"
export const Dialog = DialogPrimitive.Root
export const Icon = ChevronDownIcon
@@ -401,7 +401,7 @@ import {
export const DialogRoot = Root
export const SelectValue = Value`
const expected = `import { Root, Trigger, Content, Value, Item } from "radix-ui";
const expected = `import { Root, Trigger, Content, Value, Item } from "radix-ui"
export const DialogRoot = Root
export const SelectValue = Value`
@@ -425,7 +425,7 @@ import * as SelectPrimitive from "@radix-ui/react-select"
export const Dialog = DialogRoot
export const Select = SelectPrimitive.Root`
const expected = `import { Root as DialogRoot, Trigger, Content, Select as SelectPrimitive } from "radix-ui";
const expected = `import { Root as DialogRoot, Trigger, Content, Select as SelectPrimitive } from "radix-ui"
export const Dialog = DialogRoot
export const Select = SelectPrimitive.Root`
@@ -450,7 +450,7 @@ import {
export type Props = DialogProps`
const expected = `import { type ComponentProps, type DialogProps, type SelectProps, Root } from "radix-ui";
const expected = `import { type ComponentProps, type DialogProps, type SelectProps, Root } from "radix-ui"
export type Props = DialogProps`
@@ -472,7 +472,7 @@ export type Props = DialogProps`
export const DialogRoot = Root`
const expected = `import { Root, Trigger, Content } from "radix-ui";
const expected = `import { Root, Trigger, Content } from "radix-ui"
export const DialogRoot = Root`
@@ -490,7 +490,7 @@ export const DialogRoot = Root`
export const DialogRoot = Root`
const expected = `import { Root, Trigger, Content } from "radix-ui";
const expected = `import { Root, Trigger, Content } from "radix-ui"
export const DialogRoot = Root`
@@ -532,7 +532,7 @@ export const Accordion = AccordionPrimitive.Root
export const AlertDialog = AlertDialogPrimitive.Root
export const Button = Slot`
const expected = `import { Accordion as AccordionPrimitive, AlertDialog as AlertDialogPrimitive, AspectRatio as AspectRatioPrimitive, Avatar as AvatarPrimitive, Slot as SlotPrimitive, Checkbox as CheckboxPrimitive, Collapsible as CollapsiblePrimitive, ContextMenu as ContextMenuPrimitive, Dialog as DialogPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Label as LabelPrimitive, Menubar as MenubarPrimitive, NavigationMenu as NavigationMenuPrimitive, Popover as PopoverPrimitive, Progress as ProgressPrimitive, RadioGroup as RadioGroupPrimitive, ScrollArea as ScrollAreaPrimitive, Select as SelectPrimitive, Separator as SeparatorPrimitive, Slider as SliderPrimitive, Switch as SwitchPrimitive, Tabs as TabsPrimitive, Toggle as TogglePrimitive, ToggleGroup as ToggleGroupPrimitive, Tooltip as TooltipPrimitive } from "radix-ui";
const expected = `import { Accordion as AccordionPrimitive, AlertDialog as AlertDialogPrimitive, AspectRatio as AspectRatioPrimitive, Avatar as AvatarPrimitive, Slot as SlotPrimitive, Checkbox as CheckboxPrimitive, Collapsible as CollapsiblePrimitive, ContextMenu as ContextMenuPrimitive, Dialog as DialogPrimitive, DropdownMenu as DropdownMenuPrimitive, HoverCard as HoverCardPrimitive, Label as LabelPrimitive, Menubar as MenubarPrimitive, NavigationMenu as NavigationMenuPrimitive, Popover as PopoverPrimitive, Progress as ProgressPrimitive, RadioGroup as RadioGroupPrimitive, ScrollArea as ScrollAreaPrimitive, Select as SelectPrimitive, Separator as SeparatorPrimitive, Slider as SliderPrimitive, Switch as SwitchPrimitive, Tabs as TabsPrimitive, Toggle as TogglePrimitive, ToggleGroup as ToggleGroupPrimitive, Tooltip as TooltipPrimitive } from "radix-ui"
export const Accordion = AccordionPrimitive.Root
export const AlertDialog = AlertDialogPrimitive.Root
@@ -578,7 +578,7 @@ import { cn } from "@/lib/utils"
export const Sheet = SheetPrimitive.Root
export const SheetTrigger = SheetPrimitive.Trigger`
const expected = `import { Dialog as SheetPrimitive } from "radix-ui";
const expected = `import { Dialog as SheetPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
export const Sheet = SheetPrimitive.Root
@@ -597,7 +597,7 @@ import { Slot } from "@radix-ui/react-slot"
export const FormLabel = LabelPrimitive.Root
export const FormControl = Slot`
const expected = `import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui"
export const FormLabel = LabelPrimitive.Root
export const FormControl = SlotPrimitive.Slot`
@@ -664,7 +664,7 @@ export const FormControl = SlotPrimitive.Slot`
expect(result.replacedPackages.sort()).toEqual(allPackages.sort())
// Should be a single unified import from radix-ui
expect(result.content).toContain('from "radix-ui";')
expect(result.content).toContain('from "radix-ui"')
expect(result.content.startsWith("import {")).toBe(true)
expect(result.content).toContain("Slot as SlotPrimitive") // Slot should be aliased as SlotPrimitive
expect(result.content).toContain("Accordion as AccordionPrimitive") // Namespace should be aliased
@@ -685,7 +685,7 @@ const Button = ({ asChild, children }) => {
return <Comp>{children}</Comp>
}`
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
const Button = ({ asChild, children }) => {
const Comp = asChild ? SlotPrimitive.Slot : "button"
@@ -708,7 +708,7 @@ const Button = ({ asChild }) => {
return null
}`
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
const Button = ({ asChild }) => {
const Comp1 = asChild ? SlotPrimitive.Slot : "button"
@@ -731,7 +731,7 @@ const Button = ({ asChild }) => {
return null
}`
const expected = `import { Slot as SlotComponent } from "radix-ui";
const expected = `import { Slot as SlotComponent } from "radix-ui"
const Button = ({ asChild }) => {
const Comp = asChild ? Slot : "button"
@@ -754,7 +754,7 @@ const Button = ({ asChild }) => {
return <Slot />
}`
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
const Button = ({ asChild }) => {
const SlotName = "Slot"
@@ -781,7 +781,7 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
return <Comp {...props} />
}`
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
import React from "react"
type ButtonProps = React.ComponentProps<typeof SlotPrimitive.Slot> & {
@@ -811,7 +811,7 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
return <Comp {...props} />
}`
const expected = `import { Slot as SlotPrimitive } from "radix-ui";
const expected = `import { Slot as SlotPrimitive } from "radix-ui"
import { ComponentProps } from "react"
type ButtonProps = ComponentProps<typeof SlotPrimitive.Slot> & {
@@ -827,6 +827,46 @@ const Button = ({ asChild, ...props }: ButtonProps) => {
expect(result.content.trim()).toBe(expected.trim())
expect(result.replacedPackages).toEqual(["@radix-ui/react-slot"])
})
it("should not add double semicolons when import already ends with semicolon", async () => {
const input = `import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as SelectPrimitive from "@radix-ui/react-select";
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui";
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const result = await migrateRadixFile(input)
expect(result.content.trim()).toBe(expected.trim())
expect(result.replacedPackages).toEqual([
"@radix-ui/react-dialog",
"@radix-ui/react-select",
])
})
it("should not add semicolon when original imports don't have semicolons", async () => {
const input = `import * as DialogPrimitive from "@radix-ui/react-dialog"
import * as SelectPrimitive from "@radix-ui/react-select"
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const expected = `import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui"
export const Dialog = DialogPrimitive.Root
export const Select = SelectPrimitive.Root`
const result = await migrateRadixFile(input)
expect(result.content.trim()).toBe(expected.trim())
expect(result.replacedPackages).toEqual([
"@radix-ui/react-dialog",
"@radix-ui/react-select",
])
})
})
describe("migrateRadix - package.json updates", () => {

View File

@@ -215,13 +215,15 @@ export async function migrateRadixFile(
content: string
): Promise<{ content: string; replacedPackages: string[] }> {
// Enhanced regex to handle type-only imports, but exclude react-icons
// Also capture optional semicolon at the end
const radixImportPattern =
/import\s+(?:(type)\s+)?(?:\*\s+as\s+(\w+)|{([^}]+)})\s+from\s+(["'])@radix-ui\/react-([^"']+)\4/g
/import\s+(?:(type)\s+)?(?:\*\s+as\s+(\w+)|{([^}]+)})\s+from\s+(["'])@radix-ui\/react-([^"']+)\4(;?)/g
const imports: Array<{ name: string; alias?: string; isType?: boolean }> = []
const linesToRemove: string[] = []
const replacedPackages: string[] = []
let quoteStyle = '"' // Default to double quotes
let hasSemicolon = false // Track if any import had a semicolon
let result = content
let match
@@ -235,6 +237,7 @@ export async function migrateRadixFile(
namedImports,
quote,
packageName,
semicolon,
] = match
// Skip react-icons package and any sub-paths (like react-icons/dist/types)
@@ -244,9 +247,10 @@ export async function migrateRadixFile(
linesToRemove.push(fullMatch)
// Use the quote style from the first import
// Use the quote style and semicolon style from the first import
if (linesToRemove.length === 1) {
quoteStyle = quote
hasSemicolon = semicolon === ";"
}
// Track which package we're replacing
@@ -301,7 +305,9 @@ export async function migrateRadixFile(
})
.join(", ")
const unifiedImport = `import { ${importList} } from ${quoteStyle}radix-ui${quoteStyle};`
const unifiedImport = `import { ${importList} } from ${quoteStyle}radix-ui${quoteStyle}${
hasSemicolon ? ";" : ""
}`
// Replace first import with unified import, remove the rest
result = linesToRemove.reduce((acc, line, index) => {

View File

@@ -72,6 +72,14 @@ export const FRAMEWORKS = {
tailwind: "https://tailwindcss.com/docs/guides/gatsby",
},
},
expo: {
name: "expo",
label: "Expo",
links: {
installation: "https://ui.shadcn.com/docs/installation/expo",
tailwind: "https://www.nativewind.dev/docs/getting-started/installation",
},
},
manual: {
name: "manual",
label: "Manual",

View File

@@ -121,11 +121,10 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
// TanStack Start.
if (
configFiles.find((file) => file.startsWith("app.config."))?.length &&
[
...Object.keys(packageJson?.dependencies ?? {}),
...Object.keys(packageJson?.devDependencies ?? {}),
].find((dep) => dep.startsWith("@tanstack/start"))
].find((dep) => dep.startsWith("@tanstack/react-start"))
) {
type.framework = FRAMEWORKS["tanstack-start"]
return type
@@ -147,6 +146,12 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
return type
}
// Expo.
if (packageJson?.dependencies?.expo) {
type.framework = FRAMEWORKS["expo"]
return type
}
return type
}

View File

@@ -79,8 +79,70 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
const [, name, params] = atRuleMatch
// Special handling for plugins - place them after imports
if (name === "plugin") {
// Find existing plugin with same params
const existingPlugin = root.nodes?.find(
(node): node is AtRule =>
node.type === "atrule" &&
node.name === "plugin" &&
node.params === params
)
if (!existingPlugin) {
const pluginRule = postcss.atRule({
name: "plugin",
params,
raws: { semicolon: true, before: "\n" },
})
// Find the last import or plugin node to insert after
const importNodes = root.nodes?.filter(
(node): node is AtRule =>
node.type === "atrule" && node.name === "import"
)
const pluginNodes = root.nodes?.filter(
(node): node is AtRule =>
node.type === "atrule" && node.name === "plugin"
)
if (pluginNodes && pluginNodes.length > 0) {
// Insert after the last existing plugin
const lastPlugin = pluginNodes[pluginNodes.length - 1]
root.insertAfter(lastPlugin, pluginRule)
} else if (importNodes && importNodes.length > 0) {
// Insert after the last import if no plugins exist
const lastImport = importNodes[importNodes.length - 1]
root.insertAfter(lastImport, pluginRule)
// Add a break comment before the first plugin to create spacing
root.insertBefore(
pluginRule,
postcss.comment({ text: "---break---" })
)
// Add a break comment after the plugin for spacing from other content
root.insertAfter(
pluginRule,
postcss.comment({ text: "---break---" })
)
} else {
// If no imports or plugins, insert at the beginning
root.prepend(pluginRule)
// Add a break comment before the first plugin for spacing
root.insertBefore(
pluginRule,
postcss.comment({ text: "---break---" })
)
// Add a break comment after the plugin for spacing from other content
root.insertAfter(
pluginRule,
postcss.comment({ text: "---break---" })
)
}
}
}
// Special handling for keyframes - place them under @theme inline
if (name === "keyframes") {
else if (name === "keyframes") {
let themeInline = root.nodes?.find(
(node): node is AtRule =>
node.type === "atrule" &&

View File

@@ -30,7 +30,7 @@ export async function updateDependencies(
const dependenciesSpinner = spinner(`Installing dependencies.`, {
silent: options.silent,
})?.start()
const packageManager = await getPackageManager(config.resolvedPaths.cwd)
const packageManager = await getUpdateDependenciesPackageManager(config)
// Offer to use --force or --legacy-peer-deps if using React 19 with npm.
let flag = ""
@@ -62,38 +62,13 @@ export async function updateDependencies(
dependenciesSpinner?.start()
if (dependencies?.length) {
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
...(packageManager === "deno"
? dependencies.map((dep) => `npm:${dep}`)
: dependencies),
],
{
cwd: config.resolvedPaths.cwd,
}
)
}
if (devDependencies?.length) {
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
"-D",
...(packageManager === "deno"
? devDependencies.map((dep) => `npm:${dep}`)
: devDependencies),
],
{
cwd: config.resolvedPaths.cwd,
}
)
}
await installWithPackageManager(
packageManager,
dependencies,
devDependencies,
config.resolvedPaths.cwd,
flag
)
dependenciesSpinner?.succeed()
}
@@ -113,3 +88,107 @@ function shouldPromptForNpmFlag(config: Config) {
return hasReact19 && hasReactDayPicker8
}
async function getUpdateDependenciesPackageManager(config: Config) {
const expoVersion = getPackageInfo(config.resolvedPaths.cwd, false)
?.dependencies?.expo
if (expoVersion) {
// Ensures package versions match the React Native version.
// https://docs.expo.dev/more/expo-cli/#install
return "expo"
}
return getPackageManager(config.resolvedPaths.cwd)
}
async function installWithPackageManager(
packageManager: Awaited<
ReturnType<typeof getUpdateDependenciesPackageManager>
>,
dependencies: string[],
devDependencies: string[],
cwd: string,
flag?: string
) {
if (packageManager === "npm") {
return installWithNpm(dependencies, devDependencies, cwd, flag)
}
if (packageManager === "deno") {
return installWithDeno(dependencies, devDependencies, cwd)
}
if (packageManager === "expo") {
return installWithExpo(dependencies, devDependencies, cwd)
}
if (dependencies?.length) {
await execa(packageManager, ["add", ...dependencies], {
cwd,
})
}
if (devDependencies?.length) {
await execa(packageManager, ["add", "-D", ...devDependencies], { cwd })
}
}
async function installWithNpm(
dependencies: string[],
devDependencies: string[],
cwd: string,
flag?: string
) {
if (dependencies.length) {
await execa(
"npm",
["install", ...(flag ? [`--${flag}`] : []), ...dependencies],
{ cwd }
)
}
if (devDependencies.length) {
await execa(
"npm",
["install", ...(flag ? [`--${flag}`] : []), "-D", ...devDependencies],
{ cwd }
)
}
}
async function installWithDeno(
dependencies: string[],
devDependencies: string[],
cwd: string
) {
if (dependencies?.length) {
await execa("deno", ["add", ...dependencies.map((dep) => `npm:${dep}`)], {
cwd,
})
}
if (devDependencies?.length) {
await execa(
"deno",
["add", "-D", ...devDependencies.map((dep) => `npm:${dep}`)],
{ cwd }
)
}
}
async function installWithExpo(
dependencies: string[],
devDependencies: string[],
cwd: string
) {
if (dependencies.length) {
await execa("npx", ["expo", "install", ...dependencies], { cwd })
}
if (devDependencies.length) {
await execa("npx", ["expo", "install", "-- -D", ...devDependencies], {
cwd,
})
}
}

View File

@@ -379,7 +379,7 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
initializer &&
initializer.isKind(SyntaxKind.ArrayLiteralExpression)
) {
unnsetSpreadElements(
unsetSpreadElements(
initializer.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
}
@@ -387,7 +387,7 @@ export function unnestSpreadProperties(obj: ObjectLiteralExpression) {
}
}
export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
export function unsetSpreadElements(arr: ArrayLiteralExpression) {
const elements = arr.getElements()
for (let j = 0; j < elements.length; j++) {
const element = elements[j]
@@ -398,7 +398,7 @@ export function unnsetSpreadElements(arr: ArrayLiteralExpression) {
)
} else if (element.isKind(SyntaxKind.ArrayLiteralExpression)) {
// Recursive check on nested arrays
unnsetSpreadElements(
unsetSpreadElements(
element.asKindOrThrow(SyntaxKind.ArrayLiteralExpression)
)
} else if (element.isKind(SyntaxKind.StringLiteral)) {

View File

@@ -328,4 +328,290 @@ describe("transformCss", () => {
}"
`)
})
test("should add plugin directive", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@plugin foo": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin foo;"
`)
})
test("should group plugins together after imports", async () => {
const input = `@import "tailwindcss";
@layer base {
body {
font-family: sans-serif;
}
}
@utility content-auto {
content-visibility: auto;
}`
const result = await transformCss(input, {
"@plugin foo": {},
"@plugin bar": {},
"@layer components": {
".card": {
padding: "1rem",
},
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin foo;
@plugin bar;
@layer base {
body {
font-family: sans-serif;
}
}
@utility content-auto {
content-visibility: auto;
}
@layer components {
.card {
padding: 1rem;
}
}"
`)
})
test("should not add duplicate plugins", async () => {
const input = `@import "tailwindcss";
@plugin foo;
@layer base {
body {
font-family: sans-serif;
}
}`
const result = await transformCss(input, {
"@plugin foo": {},
"@plugin bar": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin foo;
@plugin bar;
@layer base {
body {
font-family: sans-serif;
}
}"
`)
})
test("should add plugin when no imports exist", async () => {
const input = `@layer base {
body {
font-family: sans-serif;
}
}`
const result = await transformCss(input, {
"@plugin foo": {},
})
expect(result).toMatchInlineSnapshot(`
"
@plugin foo;
@layer base {
body {
font-family: sans-serif;
}
}"
`)
})
test("should handle plugins with quoted parameters", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@plugin @tailwindcss/typography": {},
"@plugin ./custom-plugin.js": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin @tailwindcss/typography;
@plugin ./custom-plugin.js;"
`)
})
test("should handle plugins with complex parameters", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@plugin tailwindcss/plugin": {},
"@plugin @headlessui/tailwindcss": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin tailwindcss/plugin;
@plugin @headlessui/tailwindcss;"
`)
})
test("should handle multiple imports with plugins", async () => {
const input = `@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");`
const result = await transformCss(input, {
"@plugin foo": {},
"@plugin bar": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@plugin foo;
@plugin bar;"
`)
})
test("should add plugins to empty file", async () => {
const input = ``
const result = await transformCss(input, {
"@plugin foo": {},
})
expect(result).toMatchInlineSnapshot(`
"
@plugin foo"
`)
})
test("should maintain plugin order with existing plugins", async () => {
const input = `@import "tailwindcss";
@plugin existing-plugin;
@plugin another-existing;`
const result = await transformCss(input, {
"@plugin new-plugin": {},
"@plugin final-plugin": {},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@plugin existing-plugin;
@plugin another-existing;
@plugin new-plugin;
@plugin final-plugin;"
`)
})
test("should handle comprehensive CSS with plugins", async () => {
const input = `@import "tailwindcss";
@import url("fonts.css");
@layer base {
* {
box-sizing: border-box;
}
}
@utility content-auto {
content-visibility: auto;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}`
const result = await transformCss(input, {
"@plugin @tailwindcss/typography": {},
"@plugin ./custom": {},
"@layer components": {
".btn": {
padding: "0.5rem 1rem",
"&:hover": {
"background-color": "blue",
},
},
},
"@utility animate-fast": {
"animation-duration": "0.1s",
},
"@keyframes spin-fast": {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" },
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@import url("fonts.css");
@plugin @tailwindcss/typography;
@plugin ./custom;
@layer base {
* {
box-sizing: border-box;
}
}
@utility content-auto {
content-visibility: auto;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@layer components {
.btn {
padding: 0.5rem 1rem;
}
.btn:hover {
background-color: blue;
}
}
@utility animate-fast {
animation-duration: 0.1s;
}
@theme inline {
@keyframes spin-fast {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}"
`)
})
})

View File

@@ -5,7 +5,7 @@ import {
buildTailwindThemeColorsFromCssVars, nestSpreadElements,
nestSpreadProperties,
transformTailwindConfig,
unnestSpreadProperties, unnsetSpreadElements,
unnestSpreadProperties, unsetSpreadElements,
} from "../../../src/utils/updaters/update-tailwind-config"
const SHARED_CONFIG = {
@@ -1186,7 +1186,7 @@ describe("unnestSpreadElements", () => {
)
if (!configObject) throw new Error("Config object not found")
unnsetSpreadElements(configObject)
unsetSpreadElements(configObject)
const result = configObject.getText()
expect(result.replace(/\s+/g, "")).toBe(expected.replace(/\s+/g, ""))

70
pnpm-lock.yaml generated
View File

@@ -322,7 +322,7 @@ importers:
specifier: ^6.0.1
version: 6.0.1
shadcn:
specifier: 2.6.2
specifier: 2.7.0
version: link:../../packages/shadcn
shiki:
specifier: ^1.10.1
@@ -602,7 +602,7 @@ importers:
specifier: 2.12.7
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
shadcn:
specifier: 2.6.2
specifier: 2.7.0
version: link:../../packages/shadcn
sharp:
specifier: ^0.32.6
@@ -696,16 +696,6 @@ importers:
specifier: ^4.1.2
version: 4.1.2
packages/cli:
dependencies:
chalk:
specifier: ^5.4.1
version: 5.4.1
devDependencies:
tsup:
specifier: ^6.6.3
version: 6.7.0(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)
packages/shadcn:
dependencies:
'@antfu/ni':
@@ -4779,10 +4769,6 @@ packages:
resolution: {integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
chalk@5.4.1:
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
@@ -14349,8 +14335,6 @@ snapshots:
chalk@5.2.0: {}
chalk@5.4.1: {}
change-case@5.4.4: {}
character-entities-html4@2.1.0: {}
@@ -18447,14 +18431,6 @@ snapshots:
postcss: 8.5.1
ts-node: 10.9.2(@types/node@22.13.0)(typescript@4.9.5)
postcss-load-config@3.1.4(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)):
dependencies:
lilconfig: 2.1.0
yaml: 1.10.2
optionalDependencies:
postcss: 8.5.1
ts-node: 10.9.2(@types/node@22.13.0)(typescript@5.7.3)
postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@17.0.45)(typescript@5.7.3)):
dependencies:
lilconfig: 3.1.3
@@ -20100,25 +20076,6 @@ snapshots:
yn: 3.1.1
optional: true
ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.13.0
acorn: 8.14.0
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.7.3
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
optional: true
ts-pattern@5.6.2: {}
tsconfck@3.1.4(typescript@5.7.3):
@@ -20167,29 +20124,6 @@ snapshots:
- supports-color
- ts-node
tsup@6.7.0(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3):
dependencies:
bundle-require: 4.2.1(esbuild@0.17.19)
cac: 6.7.14
chokidar: 3.6.0
debug: 4.4.0
esbuild: 0.17.19
execa: 5.1.1
globby: 11.1.0
joycon: 3.1.1
postcss-load-config: 3.1.4(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))
resolve-from: 5.0.0
rollup: 3.29.5
source-map: 0.8.0-beta.0
sucrase: 3.35.0
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.1
typescript: 5.7.3
transitivePeerDependencies:
- supports-color
- ts-node
tsutils@3.21.0(typescript@5.7.3):
dependencies:
tslib: 1.14.1

View File

@@ -0,0 +1,3 @@
{
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
}