mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-19 13:51:34 +00:00
Compare commits
22 Commits
shadcn@2.6
...
shadcn@2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7443edcfb0 | ||
|
|
9d9a33be52 | ||
|
|
d544a7f7a5 | ||
|
|
48fe0d709f | ||
|
|
ed244ea0b5 | ||
|
|
b8fede1742 | ||
|
|
84d6c83bad | ||
|
|
5b8ee41511 | ||
|
|
7c3d34cdc9 | ||
|
|
56c4c83511 | ||
|
|
2821cb0e39 | ||
|
|
3c87402de2 | ||
|
|
20a88e1f15 | ||
|
|
cb19ab8464 | ||
|
|
cf1851ca09 | ||
|
|
c86c27a2ff | ||
|
|
8847126c65 | ||
|
|
65350857a4 | ||
|
|
40c7473c7e | ||
|
|
4698ee960f | ||
|
|
2ae0e5a07b | ||
|
|
431af4f7ff |
20
.github/workflows/issue-stale.yml
vendored
20
.github/workflows/issue-stale.yml
vendored
@@ -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 there’s 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 you’re 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 there’s 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 you’re 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 there’s 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 there’s 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
|
||||
|
||||
@@ -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
2
apps/v4/.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_V0_URL=https://v0.dev
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:4000
|
||||
1
apps/v4/.gitignore
vendored
1
apps/v4/.gitignore
vendored
@@ -32,6 +32,7 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Callout({
|
||||
return (
|
||||
<Alert
|
||||
className={cn(
|
||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-4",
|
||||
"bg-surface text-surface-foreground mt-6 w-auto border-none md:-mx-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function CodeCollapsibleWrapper({
|
||||
<Collapsible
|
||||
open={isOpened}
|
||||
onOpenChange={setIsOpened}
|
||||
className={cn("group/collapsible relative md:-mx-4", className)}
|
||||
className={cn("group/collapsible relative md:-mx-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
|
||||
@@ -51,7 +51,7 @@ export function ComponentPreviewTabs({
|
||||
</Tabs>
|
||||
<div
|
||||
data-tab={tab}
|
||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-4"
|
||||
className="data-[tab=code]:border-code relative rounded-lg border md:-mx-1"
|
||||
>
|
||||
<div
|
||||
data-slot="preview"
|
||||
|
||||
@@ -34,7 +34,7 @@ export function ComponentPreview({
|
||||
|
||||
if (type === "block") {
|
||||
return (
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-4">
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md border md:-mx-1">
|
||||
<Image
|
||||
src={`/r/styles/new-york-v4/${name}-light.png`}
|
||||
alt={name}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,15 +21,14 @@ Usage: shadcn init [options] [components...]
|
||||
initialize your project and install dependencies
|
||||
|
||||
Arguments:
|
||||
components the components to add or a url to the component.
|
||||
components name, url or local path to component
|
||||
|
||||
Options:
|
||||
-t, --template <template> the template to use. (next, next-monorepo)
|
||||
-b, --base-color <base-color> the base color to use. (neutral, gray, zinc, stone, slate)
|
||||
-y, --yes skip confirmation prompt. (default: true)
|
||||
-f, --force force overwrite of existing configuration. (default: false)
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-s, --silent mute output. (default: false)
|
||||
--src-dir use the src directory when creating a new project. (default: false)
|
||||
--no-src-dir do not use the src directory when creating a new project.
|
||||
@@ -54,12 +53,12 @@ Usage: shadcn add [options] [components...]
|
||||
add a component to your project
|
||||
|
||||
Arguments:
|
||||
components the components to add or a url to the component.
|
||||
components name, url or local path to component
|
||||
|
||||
Options:
|
||||
-y, --yes skip confirmation prompt. (default: false)
|
||||
-o, --overwrite overwrite existing files. (default: false)
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-a, --all add all available components (default: false)
|
||||
-p, --path <path> the path to add the component to.
|
||||
-s, --silent mute output. (default: false)
|
||||
@@ -92,8 +91,7 @@ Arguments:
|
||||
|
||||
Options:
|
||||
-o, --output <path> destination directory for json files (default: "./public/r")
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
||||
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
||||
-c, --cwd <cwd> the working directory. defaults to the current directory.
|
||||
-h, --help display help for command
|
||||
```
|
||||
|
||||
|
||||
@@ -15,3 +15,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://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"recharts": "2.15.1",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"shadcn": "2.6.2",
|
||||
"shadcn": "2.8.0",
|
||||
"shiki": "^1.10.1",
|
||||
"sonner": "^2.0.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
font-size: var(--text-sm);
|
||||
outline: none;
|
||||
position: relative;
|
||||
@apply md:-mx-4;
|
||||
@apply md:-mx-1;
|
||||
|
||||
&:has([data-rehype-pretty-code-title]) [data-slot="copy-button"] {
|
||||
top: calc(var(--spacing) * 1.5) !important;
|
||||
|
||||
6
apps/www/README.md
Normal file
6
apps/www/README.md
Normal 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).
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.8.0",
|
||||
"sharp": "^0.32.6",
|
||||
"sonner": "^1.2.3",
|
||||
"swr": "2.2.6-beta.3",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"dependencies": [
|
||||
"react-day-picker@8.10.1",
|
||||
"react-day-picker",
|
||||
"date-fns"
|
||||
],
|
||||
"registryDependencies": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"dependencies": [
|
||||
"react-day-picker@8.10.1",
|
||||
"react-day-picker",
|
||||
"date-fns"
|
||||
],
|
||||
"registryDependencies": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "calendar",
|
||||
"dependencies": [
|
||||
"react-day-picker@8.10.1",
|
||||
"react-day-picker",
|
||||
"date-fns"
|
||||
],
|
||||
"registryDependencies": [
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @shadcn/ui
|
||||
|
||||
## 2.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#7720](https://github.com/shadcn-ui/ui/pull/7720) [`d544a7f7a519cd5b171d9ee7cb2fd1a226659ece`](https://github.com/shadcn-ui/ui/commit/d544a7f7a519cd5b171d9ee7cb2fd1a226659ece) Thanks [@shadcn](https://github.com/shadcn)! - refactor registry dependencies resolution
|
||||
|
||||
- [#7717](https://github.com/shadcn-ui/ui/pull/7717) [`48fe0d709fd2b244314f95f56e7afb38b117ed8a`](https://github.com/shadcn-ui/ui/commit/48fe0d709fd2b244314f95f56e7afb38b117ed8a) Thanks [@shadcn](https://github.com/shadcn)! - add support for local registry item
|
||||
|
||||
- [#6330](https://github.com/shadcn-ui/ui/pull/6330) [`ed244ea0b5abf7db50ac5fcf26e2993133fe94f7`](https://github.com/shadcn-ui/ui/commit/ed244ea0b5abf7db50ac5fcf26e2993133fe94f7) Thanks [@KitsuneDev](https://github.com/KitsuneDev)! - add support for vinxi based framework
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shadcn",
|
||||
"version": "2.6.2",
|
||||
"version": "2.8.0",
|
||||
"description": "Add components to your apps.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from "path"
|
||||
import { runInit } from "@/src/commands/init"
|
||||
import { preFlightAdd } from "@/src/preflights/preflight-add"
|
||||
import { getRegistryIndex, getRegistryItem, isUrl } from "@/src/registry/api"
|
||||
import { getRegistryIndex, getRegistryItem } from "@/src/registry/api"
|
||||
import { registryItemTypeSchema } from "@/src/registry/schema"
|
||||
import { isLocalFile, isUrl } from "@/src/registry/utils"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
import { createProject } from "@/src/utils/create-project"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
@@ -46,10 +47,7 @@ export const addOptionsSchema = z.object({
|
||||
export const add = new Command()
|
||||
.name("add")
|
||||
.description("add a component to your project")
|
||||
.argument(
|
||||
"[components...]",
|
||||
"the components to add or a url to the component."
|
||||
)
|
||||
.argument("[components...]", "names, url or local path to component")
|
||||
.option("-y, --yes", "skip confirmation prompt.", false)
|
||||
.option("-o, --overwrite", "overwrite existing files.", false)
|
||||
.option(
|
||||
@@ -81,7 +79,10 @@ export const add = new Command()
|
||||
|
||||
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
||||
|
||||
if (components.length > 0 && isUrl(components[0])) {
|
||||
if (
|
||||
components.length > 0 &&
|
||||
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||
) {
|
||||
const item = await getRegistryItem(components[0], "")
|
||||
itemType = item?.type
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
getRegistryBaseColors,
|
||||
getRegistryItem,
|
||||
getRegistryStyles,
|
||||
isUrl,
|
||||
} from "@/src/registry/api"
|
||||
import { isLocalFile, isUrl } from "@/src/registry/utils"
|
||||
import { addComponents } from "@/src/utils/add-components"
|
||||
import { TEMPLATES, createProject } from "@/src/utils/create-project"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
@@ -82,10 +82,7 @@ export const initOptionsSchema = z.object({
|
||||
export const init = new Command()
|
||||
.name("init")
|
||||
.description("initialize your project and install dependencies")
|
||||
.argument(
|
||||
"[components...]",
|
||||
"the components to add or a url to the component."
|
||||
)
|
||||
.argument("[components...]", "names, url or local path to component")
|
||||
.option(
|
||||
"-t, --template <template>",
|
||||
"the template to use. (next, next-monorepo)"
|
||||
@@ -128,7 +125,10 @@ export const init = new Command()
|
||||
// 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.
|
||||
if (components.length > 0 && isUrl(components[0])) {
|
||||
if (
|
||||
components.length > 0 &&
|
||||
(isUrl(components[0]) || isLocalFile(components[0]))
|
||||
) {
|
||||
const item = await getRegistryItem(components[0], "")
|
||||
|
||||
// Skip base color if style.
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,12 +1,54 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { HttpResponse, http } from "msw"
|
||||
import { setupServer } from "msw/node"
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest"
|
||||
|
||||
import { clearRegistryCache, fetchRegistry } from "./api"
|
||||
import {
|
||||
clearRegistryCache,
|
||||
fetchRegistry,
|
||||
getRegistryItem,
|
||||
registryResolveItemsTree,
|
||||
} from "./api"
|
||||
|
||||
// Mock the handleError function to prevent process.exit in tests
|
||||
vi.mock("@/src/utils/handle-error", () => ({
|
||||
handleError: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the logger to prevent console output in tests
|
||||
vi.mock("@/src/utils/logger", () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
break: vi.fn(),
|
||||
log: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const REGISTRY_URL = "https://ui.shadcn.com/r"
|
||||
|
||||
const server = setupServer(
|
||||
http.get(`${REGISTRY_URL}/index.json`, () => {
|
||||
return HttpResponse.json([
|
||||
{
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
},
|
||||
{
|
||||
name: "card",
|
||||
type: "registry:ui",
|
||||
},
|
||||
])
|
||||
}),
|
||||
http.get(`${REGISTRY_URL}/styles/new-york/button.json`, () => {
|
||||
return HttpResponse.json({
|
||||
name: "button",
|
||||
@@ -112,3 +154,277 @@ describe("fetchRegistry", () => {
|
||||
expect(result[1]).toMatchObject({ name: "card" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRegistryItem with local files", () => {
|
||||
it("should read and parse a valid local JSON file", async () => {
|
||||
// Create a temporary file
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "test-component.json")
|
||||
|
||||
const componentData = {
|
||||
name: "test-component",
|
||||
type: "registry:ui",
|
||||
dependencies: ["@radix-ui/react-dialog"],
|
||||
files: [
|
||||
{
|
||||
path: "ui/test-component.tsx",
|
||||
content: "// test component content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||
|
||||
try {
|
||||
const result = await getRegistryItem(tempFile, "unused-style")
|
||||
|
||||
expect(result).toMatchObject({
|
||||
name: "test-component",
|
||||
type: "registry:ui",
|
||||
dependencies: ["@radix-ui/react-dialog"],
|
||||
files: [
|
||||
{
|
||||
path: "ui/test-component.tsx",
|
||||
content: "// test component content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
})
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle relative paths", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "relative-component.json")
|
||||
|
||||
const componentData = {
|
||||
name: "relative-component",
|
||||
type: "registry:ui",
|
||||
files: [],
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||
|
||||
try {
|
||||
// Change to temp directory to test relative path
|
||||
const originalCwd = process.cwd()
|
||||
process.chdir(tempDir)
|
||||
|
||||
const result = await getRegistryItem(
|
||||
"./relative-component.json",
|
||||
"unused-style"
|
||||
)
|
||||
|
||||
expect(result).toMatchObject({
|
||||
name: "relative-component",
|
||||
type: "registry:ui",
|
||||
})
|
||||
|
||||
process.chdir(originalCwd)
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle tilde (~) home directory paths", async () => {
|
||||
const os = await import("os")
|
||||
const homeDir = os.homedir()
|
||||
const tempFile = path.join(homeDir, "shadcn-test-tilde.json")
|
||||
|
||||
const componentData = {
|
||||
name: "tilde-component",
|
||||
type: "registry:ui",
|
||||
files: [],
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(componentData))
|
||||
|
||||
try {
|
||||
// Test with tilde path
|
||||
const tildeePath = "~/shadcn-test-tilde.json"
|
||||
const result = await getRegistryItem(tildeePath, "unused-style")
|
||||
|
||||
expect(result).toMatchObject({
|
||||
name: "tilde-component",
|
||||
type: "registry:ui",
|
||||
})
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
}
|
||||
})
|
||||
|
||||
it("should return null for non-existent files", async () => {
|
||||
const result = await getRegistryItem(
|
||||
"/non/existent/file.json",
|
||||
"unused-style"
|
||||
)
|
||||
expect(result).toBe(null)
|
||||
})
|
||||
|
||||
it("should return null for invalid JSON", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "invalid.json")
|
||||
|
||||
await fs.writeFile(tempFile, "{ invalid json }")
|
||||
|
||||
try {
|
||||
const result = await getRegistryItem(tempFile, "unused-style")
|
||||
expect(result).toBe(null)
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
it("should return null for JSON that doesn't match registry schema", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "invalid-schema.json")
|
||||
|
||||
const invalidData = {
|
||||
notAValidRegistryItem: true,
|
||||
missing: "required fields",
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(invalidData))
|
||||
|
||||
try {
|
||||
const result = await getRegistryItem(tempFile, "unused-style")
|
||||
expect(result).toBe(null)
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
|
||||
it("should still handle URLs and component names", async () => {
|
||||
// Test that existing functionality still works
|
||||
const result = await getRegistryItem("button", "new-york")
|
||||
expect(result).toMatchObject({
|
||||
name: "button",
|
||||
type: "registry:ui",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle local files with URL dependencies", async () => {
|
||||
// Mock a URL endpoint for dependency
|
||||
const dependencyUrl = "https://example.com/dependency.json"
|
||||
server.use(
|
||||
http.get(dependencyUrl, () => {
|
||||
return HttpResponse.json({
|
||||
name: "url-dependency",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "ui/url-dependency.tsx",
|
||||
content: "// url dependency content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||
|
||||
const componentData = {
|
||||
name: "component-with-url-deps",
|
||||
type: "registry:ui",
|
||||
registryDependencies: [dependencyUrl, "button"], // Mix of URL and registry name
|
||||
files: [
|
||||
{
|
||||
path: "ui/component-with-url-deps.tsx",
|
||||
content: "// component with url deps content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||
|
||||
try {
|
||||
const result = await getRegistryItem(tempFile, "unused-style")
|
||||
|
||||
expect(result).toMatchObject({
|
||||
name: "component-with-url-deps",
|
||||
type: "registry:ui",
|
||||
registryDependencies: [dependencyUrl, "button"],
|
||||
})
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("registryResolveItemsTree with URL dependencies", () => {
|
||||
it("should resolve URL dependencies from local files", async () => {
|
||||
// Mock a URL endpoint for dependency
|
||||
const dependencyUrl = "https://example.com/dependency.json"
|
||||
server.use(
|
||||
http.get(dependencyUrl, () => {
|
||||
return HttpResponse.json({
|
||||
name: "url-dependency",
|
||||
type: "registry:ui",
|
||||
files: [
|
||||
{
|
||||
path: "ui/url-dependency.tsx",
|
||||
content: "// url dependency content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-test-"))
|
||||
const tempFile = path.join(tempDir, "component-with-url-deps.json")
|
||||
|
||||
const componentData = {
|
||||
name: "component-with-url-deps",
|
||||
type: "registry:ui",
|
||||
registryDependencies: [dependencyUrl], // URL dependency
|
||||
files: [
|
||||
{
|
||||
path: "ui/component-with-url-deps.tsx",
|
||||
content: "// component with url deps content",
|
||||
type: "registry:ui",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await fs.writeFile(tempFile, JSON.stringify(componentData, null, 2))
|
||||
|
||||
try {
|
||||
const mockConfig = {
|
||||
style: "new-york",
|
||||
tailwind: { baseColor: "neutral", cssVariables: true },
|
||||
resolvedPaths: { cwd: process.cwd() },
|
||||
} as any
|
||||
|
||||
const result = await registryResolveItemsTree([tempFile], mockConfig)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.files).toBeDefined()
|
||||
// Should contain files from both the main component and its URL dependency
|
||||
const filePaths = result?.files?.map((f: any) => f.path) ?? []
|
||||
expect(filePaths).toContain("ui/component-with-url-deps.tsx")
|
||||
expect(filePaths).toContain("ui/url-dependency.tsx")
|
||||
} finally {
|
||||
// Clean up
|
||||
await fs.unlink(tempFile)
|
||||
await fs.rmdir(tempDir)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { homedir } from "os"
|
||||
import path from "path"
|
||||
import { isLocalFile } from "@/src/registry/utils"
|
||||
import { Config, getTargetStyleFromConfig } from "@/src/utils/get-config"
|
||||
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
@@ -85,6 +88,12 @@ export async function getRegistryIcons() {
|
||||
|
||||
export async function getRegistryItem(name: string, style: string) {
|
||||
try {
|
||||
// Handle local file paths
|
||||
if (isLocalFile(name)) {
|
||||
return await getLocalRegistryItem(name)
|
||||
}
|
||||
|
||||
// Handle URLs and component names
|
||||
const [result] = await fetchRegistry([
|
||||
isUrl(name) ? name : `styles/${style}/${name}.json`,
|
||||
])
|
||||
@@ -97,6 +106,26 @@ export async function getRegistryItem(name: string, style: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getLocalRegistryItem(filePath: string) {
|
||||
try {
|
||||
// Handle tilde expansion for home directory
|
||||
let expandedPath = filePath
|
||||
if (filePath.startsWith("~/")) {
|
||||
expandedPath = path.join(homedir(), filePath.slice(2))
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(expandedPath)
|
||||
const content = await fs.readFile(resolvedPath, "utf8")
|
||||
const parsed = JSON.parse(content)
|
||||
|
||||
return registryItemSchema.parse(parsed)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read local registry file: ${filePath}`)
|
||||
handleError(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRegistryBaseColors() {
|
||||
return BASE_COLORS
|
||||
}
|
||||
@@ -263,26 +292,144 @@ export function clearRegistryCache() {
|
||||
registryCache.clear()
|
||||
}
|
||||
|
||||
async function resolveDependenciesRecursively(
|
||||
dependencies: string[],
|
||||
config?: Config,
|
||||
visited: Set<string> = new Set()
|
||||
): Promise<{
|
||||
items: z.infer<typeof registryItemSchema>[]
|
||||
registryNames: string[]
|
||||
}> {
|
||||
const items: z.infer<typeof registryItemSchema>[] = []
|
||||
const registryNames: string[] = []
|
||||
|
||||
for (const dep of dependencies) {
|
||||
// Avoid infinite recursion.
|
||||
if (visited.has(dep)) {
|
||||
continue
|
||||
}
|
||||
visited.add(dep)
|
||||
|
||||
if (isUrl(dep) || isLocalFile(dep)) {
|
||||
const item = await getRegistryItem(dep, "")
|
||||
if (item) {
|
||||
items.push(item)
|
||||
if (item.registryDependencies) {
|
||||
const nested = await resolveDependenciesRecursively(
|
||||
item.registryDependencies,
|
||||
config,
|
||||
visited
|
||||
)
|
||||
items.push(...nested.items)
|
||||
registryNames.push(...nested.registryNames)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Registry name - add it to the list
|
||||
registryNames.push(dep)
|
||||
|
||||
// If we have config, we can also fetch the item to get its dependencies
|
||||
if (config) {
|
||||
const style = config.resolvedPaths?.cwd
|
||||
? await getTargetStyleFromConfig(
|
||||
config.resolvedPaths.cwd,
|
||||
config.style
|
||||
)
|
||||
: config.style
|
||||
|
||||
try {
|
||||
const item = await getRegistryItem(dep, style)
|
||||
if (item && item.registryDependencies) {
|
||||
const nested = await resolveDependenciesRecursively(
|
||||
item.registryDependencies,
|
||||
config,
|
||||
visited
|
||||
)
|
||||
items.push(...nested.items)
|
||||
registryNames.push(...nested.registryNames)
|
||||
}
|
||||
} catch (error) {
|
||||
// If we can't fetch the registry item, that's okay - we'll still include the name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { items, registryNames }
|
||||
}
|
||||
|
||||
export async function registryResolveItemsTree(
|
||||
names: z.infer<typeof registryItemSchema>["name"][],
|
||||
config: Config
|
||||
) {
|
||||
try {
|
||||
const index = await getRegistryIndex()
|
||||
if (!index) {
|
||||
return null
|
||||
// Separate local files, URLs, and registry names.
|
||||
const localFiles = names.filter((name) => isLocalFile(name))
|
||||
const urls = names.filter((name) => isUrl(name))
|
||||
const registryNames = names.filter(
|
||||
(name) => !isLocalFile(name) && !isUrl(name)
|
||||
)
|
||||
|
||||
const payload: z.infer<typeof registryItemSchema>[] = []
|
||||
|
||||
// Handle local files and URLs directly, collecting their dependencies.
|
||||
const allDependencies: string[] = []
|
||||
|
||||
for (const localFile of localFiles) {
|
||||
const item = await getRegistryItem(localFile, "")
|
||||
if (item) {
|
||||
payload.push(item)
|
||||
if (item.registryDependencies) {
|
||||
allDependencies.push(...item.registryDependencies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're resolving the index, we want it to go first.
|
||||
if (names.includes("index")) {
|
||||
names.unshift("index")
|
||||
for (const url of urls) {
|
||||
const item = await getRegistryItem(url, "")
|
||||
if (item) {
|
||||
payload.push(item)
|
||||
if (item.registryDependencies) {
|
||||
allDependencies.push(...item.registryDependencies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let registryItems = await resolveRegistryItems(names, config)
|
||||
let result = await fetchRegistry(registryItems)
|
||||
const payload = z.array(registryItemSchema).parse(result)
|
||||
// Recursively resolve all dependencies.
|
||||
const { items: dependencyItems, registryNames: dependencyRegistryNames } =
|
||||
await resolveDependenciesRecursively(allDependencies, config)
|
||||
|
||||
if (!payload) {
|
||||
payload.push(...dependencyItems)
|
||||
|
||||
// Handle registry names using existing resolveRegistryItems logic.
|
||||
const allRegistryNames = [...registryNames, ...dependencyRegistryNames]
|
||||
if (allRegistryNames.length > 0) {
|
||||
const index = await getRegistryIndex()
|
||||
if (!index) {
|
||||
// If we only have local files or URLs, that's fine.
|
||||
if (payload.length === 0) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// Remove duplicates.
|
||||
const uniqueRegistryNames = Array.from(new Set(allRegistryNames))
|
||||
|
||||
// If we're resolving the index, we want it to go first.
|
||||
if (uniqueRegistryNames.includes("index")) {
|
||||
uniqueRegistryNames.unshift("index")
|
||||
}
|
||||
|
||||
let registryItems = await resolveRegistryItems(
|
||||
uniqueRegistryNames,
|
||||
config
|
||||
)
|
||||
let result = await fetchRegistry(registryItems)
|
||||
const registryPayload = z.array(registryItemSchema).parse(result)
|
||||
payload.push(...registryPayload)
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -290,7 +437,7 @@ export async function registryResolveItemsTree(
|
||||
// the theme item if a base color is provided.
|
||||
// We do this for index only.
|
||||
// Other components will ship with their theme tokens.
|
||||
if (names.includes("index")) {
|
||||
if (allRegistryNames.includes("index")) {
|
||||
if (config.tailwind.baseColor) {
|
||||
const theme = await registryGetTheme(config.tailwind.baseColor, config)
|
||||
if (theme) {
|
||||
@@ -352,44 +499,17 @@ async function resolveRegistryDependencies(
|
||||
url: string,
|
||||
config: Config
|
||||
): Promise<string[]> {
|
||||
const visited = new Set<string>()
|
||||
const payload: string[] = []
|
||||
const { registryNames } = await resolveDependenciesRecursively([url], config)
|
||||
|
||||
const style = config.resolvedPaths?.cwd
|
||||
? await getTargetStyleFromConfig(config.resolvedPaths.cwd, config.style)
|
||||
: config.style
|
||||
|
||||
async function resolveDependencies(itemUrl: string) {
|
||||
const url = getRegistryUrl(
|
||||
isUrl(itemUrl) ? itemUrl : `styles/${style}/${itemUrl}.json`
|
||||
)
|
||||
const urls = registryNames.map((name) =>
|
||||
getRegistryUrl(isUrl(name) ? name : `styles/${style}/${name}.json`)
|
||||
)
|
||||
|
||||
if (visited.has(url)) {
|
||||
return
|
||||
}
|
||||
|
||||
visited.add(url)
|
||||
|
||||
try {
|
||||
const [result] = await fetchRegistry([url])
|
||||
const item = registryItemSchema.parse(result)
|
||||
payload.push(url)
|
||||
|
||||
if (item.registryDependencies) {
|
||||
for (const dependency of item.registryDependencies) {
|
||||
await resolveDependencies(dependency)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error fetching or parsing registry item at ${itemUrl}:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await resolveDependencies(url)
|
||||
return Array.from(new Set(payload))
|
||||
return Array.from(new Set(urls))
|
||||
}
|
||||
|
||||
export async function registryGetTheme(name: string, config: Config) {
|
||||
@@ -495,7 +615,13 @@ export function isUrl(path: string) {
|
||||
// TODO: We're double-fetching here. Use a cache.
|
||||
export async function resolveRegistryItems(names: string[], config: Config) {
|
||||
let registryDependencies: string[] = []
|
||||
for (const name of names) {
|
||||
|
||||
// Filter out local files and URLs - these should be handled directly by getRegistryItem
|
||||
const registryNames = names.filter(
|
||||
(name) => !isLocalFile(name) && !isUrl(name)
|
||||
)
|
||||
|
||||
for (const name of registryNames) {
|
||||
const itemRegistryDependencies = await resolveRegistryDependencies(
|
||||
name,
|
||||
config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { getDependencyFromModuleSpecifier } from "./utils"
|
||||
import { getDependencyFromModuleSpecifier, isLocalFile, isUrl } from "./utils"
|
||||
|
||||
describe("getDependencyFromModuleSpecifier", () => {
|
||||
it("should return the first part of a non-scoped package with path", () => {
|
||||
@@ -74,3 +74,59 @@ describe("getDependencyFromModuleSpecifier", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isUrl", () => {
|
||||
it("should return true for valid URLs", () => {
|
||||
expect(isUrl("https://example.com")).toBe(true)
|
||||
expect(isUrl("http://example.com")).toBe(true)
|
||||
expect(isUrl("https://example.com/path")).toBe(true)
|
||||
expect(isUrl("https://subdomain.example.com")).toBe(true)
|
||||
expect(isUrl("https://ui.shadcn.com/r/styles/new-york/button.json")).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should return false for non-URLs", () => {
|
||||
expect(isUrl("./local-file.json")).toBe(false)
|
||||
expect(isUrl("../relative/path.json")).toBe(false)
|
||||
expect(isUrl("/absolute/path.json")).toBe(false)
|
||||
expect(isUrl("component-name")).toBe(false)
|
||||
expect(isUrl("")).toBe(false)
|
||||
expect(isUrl("just-text")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isLocalFile", () => {
|
||||
it("should return true for local JSON files", () => {
|
||||
expect(isLocalFile("./component.json")).toBe(true)
|
||||
expect(isLocalFile("../shared/button.json")).toBe(true)
|
||||
expect(isLocalFile("/absolute/path/card.json")).toBe(true)
|
||||
expect(isLocalFile("local-component.json")).toBe(true)
|
||||
expect(isLocalFile("nested/directory/dialog.json")).toBe(true)
|
||||
expect(isLocalFile("~/Desktop/component.json")).toBe(true)
|
||||
expect(isLocalFile("~/Documents/shared/button.json")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for URLs ending with .json", () => {
|
||||
expect(isLocalFile("https://example.com/component.json")).toBe(false)
|
||||
expect(isLocalFile("http://registry.com/button.json")).toBe(false)
|
||||
expect(
|
||||
isLocalFile("https://ui.shadcn.com/r/styles/new-york/button.json")
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for non-JSON files", () => {
|
||||
expect(isLocalFile("./component.tsx")).toBe(false)
|
||||
expect(isLocalFile("../shared/button.ts")).toBe(false)
|
||||
expect(isLocalFile("/absolute/path/card.js")).toBe(false)
|
||||
expect(isLocalFile("local-component.css")).toBe(false)
|
||||
expect(isLocalFile("component-name")).toBe(false)
|
||||
expect(isLocalFile("")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for directory paths", () => {
|
||||
expect(isLocalFile("./components/")).toBe(false)
|
||||
expect(isLocalFile("../shared")).toBe(false)
|
||||
expect(isLocalFile("/absolute/path")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -242,3 +242,17 @@ function determineFileType(
|
||||
|
||||
return "registry:component"
|
||||
}
|
||||
|
||||
// Additional utility functions for local file support
|
||||
export function isUrl(path: string) {
|
||||
try {
|
||||
new URL(path)
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isLocalFile(path: string) {
|
||||
return path.endsWith(".json") && !isUrl(path)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,26 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
return type
|
||||
}
|
||||
|
||||
// Vinxi-based (such as @tanstack/start and @solidjs/solid-start)
|
||||
// They are vite-based, and the same configurations used for Vite should work flawlessly
|
||||
const appConfig = configFiles.find((file) => file.startsWith("app.config"))
|
||||
if (appConfig?.length) {
|
||||
const appConfigContents = await fs.readFile(
|
||||
path.resolve(cwd, appConfig),
|
||||
"utf8"
|
||||
)
|
||||
if (appConfigContents.includes("defineConfig")) {
|
||||
type.framework = FRAMEWORKS["vite"]
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
// Expo.
|
||||
if (packageJson?.dependencies?.expo) {
|
||||
type.framework = FRAMEWORKS["expo"]
|
||||
return type
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
70
pnpm-lock.yaml
generated
@@ -322,7 +322,7 @@ importers:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
shadcn:
|
||||
specifier: 2.6.2
|
||||
specifier: 2.8.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.8.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
|
||||
|
||||
3
templates/monorepo-next/.vscode/settings.json
vendored
Normal file
3
templates/monorepo-next/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css"
|
||||
}
|
||||
Reference in New Issue
Block a user