Compare commits

...

14 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
38 changed files with 557 additions and 136 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.3",
"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.3",
"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,23 @@
# @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

View File

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

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.3
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.3
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"
}