mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 22:45:47 +00:00
Merge branch 'main' into shadcn/init-preset
This commit is contained in:
5
.changeset/tiny-moons-hang.md
Normal file
5
.changeset/tiny-moons-hang.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
fix handling of nested aschild transforms
|
||||
@@ -77,6 +77,12 @@
|
||||
"url": "https://r.assistant-ui.com/{name}.json",
|
||||
"description": "Radix-style React primitives for AI chat with adapters for AI SDK, LangGraph, Mastra, and custom backends."
|
||||
},
|
||||
{
|
||||
"name": "@tool-ui",
|
||||
"homepage": "https://www.tool-ui.com",
|
||||
"url": "https://www.tool-ui.com/r/{name}.json",
|
||||
"description": "Open source React components for rendering AI tool call widgets and rich assistant outputs."
|
||||
},
|
||||
{
|
||||
"name": "@better-upload",
|
||||
"homepage": "https://better-upload.com",
|
||||
@@ -428,8 +434,8 @@
|
||||
{
|
||||
"name": "@reui",
|
||||
"homepage": "https://reui.io",
|
||||
"url": "https://reui.io/r/{name}.json",
|
||||
"description": "Open-source collection of UI components and animated effects built with React, Typescript, Tailwind CSS, and Motion. Pairs beautifully with shadcn/ui."
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"description": "Free & open-source library of 1,000+ components and patterns to 10x your productivity in shadcn projects."
|
||||
},
|
||||
{
|
||||
"name": "@scrollxui",
|
||||
@@ -455,6 +461,12 @@
|
||||
"url": "https://roiui.com/r/{name}.json",
|
||||
"description": "Roi UI is a library that offers UI components and blocks built with Base UI primitives. Some blocks and components use motion (framer). Everything is open-source and will be forever."
|
||||
},
|
||||
{
|
||||
"name": "@slide-cn",
|
||||
"homepage": "https://slide-cn.com",
|
||||
"url": "https://slide-cn.com/r/{name}.json",
|
||||
"description": "A component library to build slide decks using code."
|
||||
},
|
||||
{
|
||||
"name": "@satoriui",
|
||||
"homepage": "https://satoriui.site",
|
||||
@@ -749,4 +761,4 @@
|
||||
"url": "https://sona-ui.vercel.app/r/{name}.json",
|
||||
"description": "A modern UI component library built with React and TailwindCSS to help you build beautiful and accessible web applications faster."
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
"files": [],
|
||||
"cssVars": {},
|
||||
"css": {
|
||||
"@import \"shadcn/tailwind.css\"": {}
|
||||
"@import \"tw-animate-css\"": {},
|
||||
"@import \"shadcn/tailwind.css\"": {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {}
|
||||
},
|
||||
"body": {
|
||||
"@apply bg-background text-foreground": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "registry:style"
|
||||
}
|
||||
@@ -19,7 +19,16 @@
|
||||
"files": [],
|
||||
"cssVars": {},
|
||||
"css": {
|
||||
"@import \"shadcn/tailwind.css\"": {}
|
||||
"@import \"tw-animate-css\"": {},
|
||||
"@import \"shadcn/tailwind.css\"": {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {}
|
||||
},
|
||||
"body": {
|
||||
"@apply bg-background text-foreground": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "registry:style"
|
||||
},
|
||||
@@ -40,7 +49,16 @@
|
||||
"files": [],
|
||||
"cssVars": {},
|
||||
"css": {
|
||||
"@import \"shadcn/tailwind.css\"": {}
|
||||
"@import \"tw-animate-css\"": {},
|
||||
"@import \"shadcn/tailwind.css\"": {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {}
|
||||
},
|
||||
"body": {
|
||||
"@apply bg-background text-foreground": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "registry:style"
|
||||
},
|
||||
|
||||
@@ -16,7 +16,16 @@
|
||||
"files": [],
|
||||
"cssVars": {},
|
||||
"css": {
|
||||
"@import \"shadcn/tailwind.css\"": {}
|
||||
"@import \"tw-animate-css\"": {},
|
||||
"@import \"shadcn/tailwind.css\"": {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {}
|
||||
},
|
||||
"body": {
|
||||
"@apply bg-background text-foreground": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "registry:style"
|
||||
}
|
||||
@@ -90,6 +90,13 @@
|
||||
"description": "Radix-style React primitives for AI chat with adapters for AI SDK, LangGraph, Mastra, and custom backends.",
|
||||
"logo": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 32 32\"><rect width=\"32\" height=\"32\" fill=\"var(--foreground)\" /><g transform=\"translate(4,4)\" fill=\"var(--foreground)\" stroke=\"var(--background)\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2z\" /><path d=\"M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1\" /></g></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@tool-ui",
|
||||
"homepage": "https://www.tool-ui.com",
|
||||
"url": "https://www.tool-ui.com/r/{name}.json",
|
||||
"description": "Open source React components for rendering AI tool call widgets and rich assistant outputs.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='var(--foreground)' d='M12 1 21.526 6.5v11L12 23 2.474 17.5v-11z'/><circle cx='12' cy='12' r='5' fill='var(--background)'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@better-upload",
|
||||
"homepage": "https://better-upload.com",
|
||||
@@ -499,8 +506,8 @@
|
||||
{
|
||||
"name": "@reui",
|
||||
"homepage": "https://reui.io",
|
||||
"url": "https://reui.io/r/{name}.json",
|
||||
"description": "Open-source collection of UI components and animated effects built with React, Typescript, Tailwind CSS, and Motion. Pairs beautifully with shadcn/ui.",
|
||||
"url": "https://reui.io/r/{style}/{name}.json",
|
||||
"description": "Free & open-source library of 1,000+ components and patterns to 10x your productivity in shadcn projects.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' width='101' height='100' fill='none' viewBox='0 0 101 100'><g fill='var(--foreground)' clip-path='url(#a)'><path fill-rule='evenodd' d='M67.167 3.846H33.833c-16.285 0-29.487 13.202-29.487 29.487v33.334c0 16.285 13.202 29.487 29.487 29.487h33.334c16.285 0 29.487-13.202 29.487-29.487V33.333c0-16.285-13.202-29.487-29.487-29.487M33.833 0C15.423 0 .5 14.924.5 33.333v33.334C.5 85.077 15.424 100 33.833 100h33.334c18.41 0 33.333-14.924 33.333-33.333V33.333C100.5 14.923 85.576 0 67.167 0z' clip-rule='evenodd' opacity='.2'/><path d='M70.636 34.396a4.698 4.698 0 1 0 0-9.396 4.698 4.698 0 0 0 0 9.396'/><path fill-rule='evenodd' d='M25.672 56.879V29.698a4.698 4.698 0 1 1 9.396 0v27.181a8.725 8.725 0 0 0 8.725 8.725h13.423a8.725 8.725 0 0 0 8.724-8.725V43.59a4.698 4.698 0 1 1 9.396 0v13.29c0 10.008-8.113 18.12-18.12 18.12H43.793c-10.008 0-18.121-8.113-18.121-18.121' clip-rule='evenodd'/></g><defs><clipPath id='a'><path fill='var(--background)' d='M0 0h101v100H0z'/></clipPath></defs></svg>"
|
||||
},
|
||||
{
|
||||
@@ -531,6 +538,13 @@
|
||||
"description": "Roi UI is a library that offers UI components and blocks built with Base UI primitives. Some blocks and components use motion (framer). Everything is open-source and will be forever.",
|
||||
"logo": "<svg xmlns='http://www.w3.org/2000/svg' id='Layer_2' data-name='Layer 2' viewBox='0 0 463.07 463.07'><defs><style>.cls-1{fill:var(--muted);stroke:var(--foreground);stroke-miterlimit:10;stroke-width:12px}</style></defs><g id='Layer_1-2' data-name='Layer 1'><path d='M459.07 231.54c0 41.45-11.07 80.3-30.44 113.76H231.54c6.42 0 12.72-.52 18.86-1.57 53.85-8.99 94.91-55.81 94.91-112.2s-41.05-103.21-94.91-112.2c-6.14-1.05-12.45-1.57-18.86-1.57V4c26.62 0 52.15 4.58 75.88 12.97a225 225 0 0 1 37.89 17.47 227.1 227.1 0 0 1 56.88 46.63 225 225 0 0 1 26.44 36.7c19.37 33.47 30.44 72.31 30.44 113.77Z' class='cls-1'/><path d='M345.31 231.54c0 56.38-41.05 103.21-94.91 112.2a113 113 0 0 1-18.86 1.57c-62.82 0-113.77-50.95-113.77-113.76s50.95-113.77 113.77-113.77c6.42 0 12.72.52 18.86 1.57 53.85 8.99 94.91 55.81 94.91 112.2Z' class='cls-1'/><path d='M231.53 4v113.77c-62.82 0-113.77 50.95-113.77 113.77V345.3H4V231.54c0-41.46 11.07-80.3 30.44-113.77 19.97-34.54 48.78-63.35 83.32-83.33C151.24 15.08 190.08 4 231.53 4Z' class='cls-1'/></g></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@slide-cn",
|
||||
"homepage": "https://slide-cn.com",
|
||||
"url": "https://slide-cn.com/r/{name}.json",
|
||||
"description": "A component library to build slide decks using code.",
|
||||
"logo": "<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'><path d='M0 0h512v512H0z' fill='#222222'/><path d='M252.128 99.616q3.552 0.016 7.088 -0.016c25.168 -0.032 50.576 6.56 69.776 23.408l2.304 1.824c13.152 10.784 20.256 26.096 24.704 42.176l0.608 1.952c0.56 2.96 0.384 6.032 0.384 9.056h-44l-4 -13.008c-9.216 -15.072 -21.792 -24.688 -38.992 -29.088 -18.672 -3.984 -39.104 -2.016 -55.328 8.176 -10.352 7.328 -16.416 17.424 -18.688 29.904a64 64 0 0 0 -0.368 8.256v2.224c0.272 12.272 5.104 21.984 13.872 30.448 7.488 6.224 16.16 10.16 25.216 13.456l2.48 0.912c8.336 2.992 16.8 5.456 25.312 7.856 57.472 16.224 57.472 16.224 74.464 31.856l2.352 2.112c5.664 5.344 9.84 11.104 13.648 17.888l1.216 2.096c8.48 16.576 8.72 41.984 3.184 59.376 -7.6 19.216 -18.224 33.824 -35.392 45.52l-2.304 1.6c-12.512 8.128 -26.96 12.4 -41.664 14.4l-2.016 0.288c-14.816 1.888 -31.424 2.144 -45.984 -1.28l-2.096 -0.464c-25.472 -5.632 -49.136 -18.096 -63.776 -40.416A78.4 78.4 0 0 1 152 328h43.008l3.008 12c4.624 14.112 13.488 25.12 26.56 32.304 16.8 7.728 40.816 9.12 58.304 3.024 15.056 -6.368 26.592 -13.152 33.2 -28.864 4.48 -12.976 4.608 -26.848 -1.072 -39.472a48 48 0 0 0 -7.008 -9.008l-1.952 -2.08c-15.568 -15.136 -40 -19.376 -60.176 -25.072 -67.12 -18.928 -67.12 -18.928 -81.84 -44.56a88 88 0 0 1 -7.408 -66.32c3.52 -10.608 8.864 -20.672 16.4 -28.976l1.904 -2.192c9.968 -10.736 22.496 -17.712 36.096 -22.816l2.304 -0.928c12.496 -4.64 25.632 -5.568 38.816 -5.472' fill='#FDFDFD'/><path d='M511.008 471.008h1.008v41.008h-41.008a112 112 0 0 1 9.696 -4.192c13.968 -5.824 22.432 -16.112 28.304 -29.824a64 64 0 0 0 2 -7.008' fill='#000000'/><path d='M0 471.008c1.616 3.216 2.896 6.336 4.192 9.696 5.808 13.968 16.096 22.432 29.808 28.304 2.336 0.816 4.576 1.456 7.008 2v1.008H0z' fill='#000000'/><path d='M471.008 0h41.008v41.008a112 112 0 0 1 -4.192 -9.68c-5.824 -14 -16.112 -22.464 -29.824 -28.32a64 64 0 0 0 -7.008 -2z' fill='#000000'/><path d='M0 0h41.008a112 112 0 0 1 -9.68 4.192C17.328 10 8.864 20.288 3.008 34a64 64 0 0 0 -2 7.008H0z' fill='#000000'/></svg>"
|
||||
},
|
||||
{
|
||||
"name": "@satoriui",
|
||||
"homepage": "https://satoriui.site",
|
||||
|
||||
@@ -26,7 +26,16 @@ const NEW_YORK_V4_STYLE = {
|
||||
devDependencies: ["tw-animate-css", "shadcn"],
|
||||
registryDependencies: ["utils"],
|
||||
css: {
|
||||
'@import "tw-animate-css"': {},
|
||||
'@import "shadcn/tailwind.css"': {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {},
|
||||
},
|
||||
body: {
|
||||
"@apply bg-background text-foreground": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
cssVars: {},
|
||||
files: [],
|
||||
|
||||
@@ -90,20 +90,19 @@ export const add = new Command()
|
||||
}
|
||||
|
||||
let itemType: z.infer<typeof registryItemTypeSchema> | undefined
|
||||
let shouldInstallBaseStyle = true
|
||||
let shouldInstallStyleIndex = true
|
||||
if (components.length > 0) {
|
||||
const [registryItem] = await getRegistryItems([components[0]], {
|
||||
config: initialConfig,
|
||||
})
|
||||
itemType = registryItem?.type
|
||||
shouldInstallBaseStyle =
|
||||
itemType !== "registry:theme" && itemType !== "registry:style"
|
||||
shouldInstallStyleIndex =
|
||||
itemType !== "registry:theme" &&
|
||||
itemType !== "registry:style" &&
|
||||
itemType !== "registry:base"
|
||||
|
||||
if (isUniversalRegistryItem(registryItem)) {
|
||||
await addComponents(components, initialConfig, {
|
||||
...options,
|
||||
baseStyle: shouldInstallBaseStyle,
|
||||
})
|
||||
await addComponents(components, initialConfig, options)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,8 +179,8 @@ export const add = new Command()
|
||||
isNewProject: false,
|
||||
srcDir: options.srcDir,
|
||||
cssVariables: options.cssVariables,
|
||||
baseStyle: shouldInstallBaseStyle,
|
||||
baseColor: shouldInstallBaseStyle ? undefined : "neutral",
|
||||
installStyleIndex: shouldInstallStyleIndex,
|
||||
baseColor: shouldInstallStyleIndex ? undefined : "neutral",
|
||||
components: options.components,
|
||||
})
|
||||
initHasRun = true
|
||||
@@ -216,8 +215,8 @@ export const add = new Command()
|
||||
isNewProject: true,
|
||||
srcDir: options.srcDir,
|
||||
cssVariables: options.cssVariables,
|
||||
baseStyle: shouldInstallBaseStyle,
|
||||
baseColor: shouldInstallBaseStyle ? undefined : "neutral",
|
||||
installStyleIndex: shouldInstallStyleIndex,
|
||||
baseColor: shouldInstallStyleIndex ? undefined : "neutral",
|
||||
components: options.components,
|
||||
})
|
||||
initHasRun = true
|
||||
@@ -244,10 +243,7 @@ export const add = new Command()
|
||||
config = updatedConfig
|
||||
|
||||
if (!initHasRun) {
|
||||
await addComponents(options.components, config, {
|
||||
...options,
|
||||
baseStyle: shouldInstallBaseStyle,
|
||||
})
|
||||
await addComponents(options.components, config, options)
|
||||
}
|
||||
|
||||
// If we're adding a single component and it's from the v0 registry,
|
||||
|
||||
@@ -205,7 +205,7 @@ export const create = new Command()
|
||||
rtl: opts.rtl,
|
||||
template,
|
||||
baseColor,
|
||||
baseStyle: false,
|
||||
installStyleIndex: false,
|
||||
registryBaseConfig,
|
||||
skipPreflight: false,
|
||||
})
|
||||
@@ -219,7 +219,6 @@ export const create = new Command()
|
||||
components.push("direction")
|
||||
}
|
||||
await addComponents(components, config, {
|
||||
baseStyle: false,
|
||||
silent: true,
|
||||
overwrite: true,
|
||||
})
|
||||
|
||||
@@ -111,7 +111,7 @@ export const initOptionsSchema = z.object({
|
||||
).join("', '")}'`,
|
||||
}
|
||||
),
|
||||
baseStyle: z.boolean(),
|
||||
installStyleIndex: z.boolean(),
|
||||
// Config from registry:base item to merge into components.json.
|
||||
registryBaseConfig: rawConfigSchema.deepPartial().optional(),
|
||||
})
|
||||
@@ -261,6 +261,7 @@ export const init = new Command()
|
||||
isNewProject: false,
|
||||
components,
|
||||
...opts,
|
||||
installStyleIndex: opts.baseStyle,
|
||||
})
|
||||
|
||||
await loadEnvFiles(options.cwd)
|
||||
@@ -332,8 +333,8 @@ export const init = new Command()
|
||||
// Store config to be merged into components.json later.
|
||||
options.registryBaseConfig = item.config
|
||||
}
|
||||
options.baseStyle =
|
||||
item.extends === "none" ? false : options.baseStyle
|
||||
options.installStyleIndex =
|
||||
item.extends === "none" ? false : options.installStyleIndex
|
||||
}
|
||||
|
||||
if (item?.type === "registry:style") {
|
||||
@@ -342,14 +343,13 @@ export const init = new Command()
|
||||
options.baseColor = "neutral"
|
||||
|
||||
// If the style extends none, we don't want to install the base style.
|
||||
options.baseStyle =
|
||||
item.extends === "none" ? false : options.baseStyle
|
||||
options.installStyleIndex =
|
||||
item.extends === "none" ? false : options.installStyleIndex
|
||||
}
|
||||
}
|
||||
|
||||
// If --no-base-style, we don't want to prompt for a base color either.
|
||||
// The style will extend or override it.
|
||||
if (!options.baseStyle) {
|
||||
if (!options.installStyleIndex) {
|
||||
options.baseColor = "neutral"
|
||||
}
|
||||
|
||||
@@ -457,7 +457,7 @@ export async function runInit(
|
||||
// Why index? Because when style is true, we read style from components.json and fetch that.
|
||||
// i.e new-york from components.json then fetch /styles/new-york/index.
|
||||
// TODO: Fix this so that we can extend any style i.e --style=new-york.
|
||||
...(options.baseStyle ? ["index"] : []),
|
||||
...(options.installStyleIndex ? ["index"] : []),
|
||||
...(options.components ?? []),
|
||||
]
|
||||
|
||||
@@ -530,7 +530,6 @@ export async function runInit(
|
||||
// Init will always overwrite files.
|
||||
overwrite: true,
|
||||
silent: options.silent,
|
||||
baseStyle: options.baseStyle,
|
||||
isNewProject:
|
||||
options.isNewProject || projectInfo?.framework.name === "next-app",
|
||||
})
|
||||
|
||||
@@ -38,7 +38,6 @@ export async function addComponents(
|
||||
overwrite?: boolean
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
baseStyle?: boolean
|
||||
registryHeaders?: Record<string, Record<string, string>>
|
||||
path?: string
|
||||
}
|
||||
@@ -47,7 +46,6 @@ export async function addComponents(
|
||||
overwrite: false,
|
||||
silent: false,
|
||||
isNewProject: false,
|
||||
baseStyle: true,
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -74,11 +72,10 @@ async function addProjectComponents(
|
||||
overwrite?: boolean
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
baseStyle?: boolean
|
||||
path?: string
|
||||
}
|
||||
) {
|
||||
if (!options.baseStyle && !components.length) {
|
||||
if (!components.length) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -117,7 +114,6 @@ async function addProjectComponents(
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
initIndex: options.baseStyle,
|
||||
})
|
||||
|
||||
// Add CSS updater
|
||||
@@ -157,11 +153,10 @@ async function addWorkspaceComponents(
|
||||
silent?: boolean
|
||||
isNewProject?: boolean
|
||||
isRemote?: boolean
|
||||
baseStyle?: boolean
|
||||
path?: string
|
||||
}
|
||||
) {
|
||||
if (!options.baseStyle && !components.length) {
|
||||
if (!components.length) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function Component() {
|
||||
})
|
||||
|
||||
describe("DialogTrigger with non-Button child", () => {
|
||||
test("transforms asChild to render prop with nativeButton={false}", async () => {
|
||||
test("transforms asChild to render prop without nativeButton", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
@@ -86,7 +86,7 @@ export function Component() {
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<DialogTrigger render={<a href="#" />} nativeButton={false}>Open Dialog</DialogTrigger>
|
||||
<DialogTrigger render={<a href="#" />}>Open Dialog</DialogTrigger>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
@@ -186,7 +186,7 @@ export function Component() {
|
||||
})
|
||||
})
|
||||
|
||||
describe("component with Link child", () => {
|
||||
describe("Button with Link child", () => {
|
||||
test("transforms asChild to render prop with nativeButton={false}", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
@@ -390,6 +390,173 @@ export function Component() {
|
||||
})
|
||||
})
|
||||
|
||||
describe("nested asChild", () => {
|
||||
test("transforms inner asChild first, then outer", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
filename: "test.tsx",
|
||||
raw: `import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<Collapsible asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href="#">Home</a>
|
||||
</SidebarMenuButton>
|
||||
</Collapsible>
|
||||
)
|
||||
}`,
|
||||
config: testConfig,
|
||||
},
|
||||
[transformAsChild]
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<Collapsible render={<SidebarMenuButton render={<a href="#" />} />}>Home</Collapsible>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("adds nativeButton={false} only on nested Button", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
filename: "test.tsx",
|
||||
raw: `import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<DialogTrigger asChild>
|
||||
<Button asChild>
|
||||
<a href="#">Open</a>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}`,
|
||||
config: testConfig,
|
||||
},
|
||||
[transformAsChild]
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<DialogTrigger render={<Button render={<a href="#" />} nativeButton={false} />}>Open</DialogTrigger>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("transforms nested with sibling asChild elements", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
filename: "test.tsx",
|
||||
raw: `import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<div>
|
||||
<Collapsible asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href="#">Home</a>
|
||||
</SidebarMenuButton>
|
||||
</Collapsible>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Edit</Button>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}`,
|
||||
config: testConfig,
|
||||
},
|
||||
[transformAsChild]
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<div>
|
||||
<Collapsible render={<SidebarMenuButton render={<a href="#" />} />}>Home</Collapsible>
|
||||
<DialogTrigger render={<Button variant="outline" />}>Edit</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("transforms nested with self-closing inner child", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
filename: "test.tsx",
|
||||
raw: `import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<Collapsible asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<Icon className="size-4" />
|
||||
</SidebarMenuButton>
|
||||
</Collapsible>
|
||||
)
|
||||
}`,
|
||||
config: testConfig,
|
||||
},
|
||||
[transformAsChild]
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<Collapsible render={<SidebarMenuButton render={<Icon className="size-4" />} />}></Collapsible>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("transforms triple-nested asChild", async () => {
|
||||
expect(
|
||||
await transform(
|
||||
{
|
||||
filename: "test.tsx",
|
||||
raw: `import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<TooltipTrigger asChild>
|
||||
<Collapsible asChild>
|
||||
<SidebarMenuButton asChild>
|
||||
<a href="#">Home</a>
|
||||
</SidebarMenuButton>
|
||||
</Collapsible>
|
||||
</TooltipTrigger>
|
||||
)
|
||||
}`,
|
||||
config: testConfig,
|
||||
},
|
||||
[transformAsChild]
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
"import * as React from "react"
|
||||
|
||||
export function Component() {
|
||||
return (
|
||||
<TooltipTrigger render={<Collapsible render={<SidebarMenuButton render={<a href="#" />} />} />}>Home</TooltipTrigger>
|
||||
)
|
||||
}"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("idempotency", () => {
|
||||
test("running twice produces same output", async () => {
|
||||
const input = `import * as React from "react"
|
||||
|
||||
@@ -27,118 +27,142 @@ export const transformAsChild: Transformer = async ({ sourceFile, config }) => {
|
||||
return sourceFile
|
||||
}
|
||||
|
||||
// Collect all transformations first, then apply them in reverse order.
|
||||
// This prevents issues with invalidated nodes when modifying the tree.
|
||||
const transformations: TransformInfo[] = []
|
||||
// Process asChild elements iteratively, starting from leaf-level elements.
|
||||
// Each iteration transforms only elements with no asChild descendants,
|
||||
// ensuring inner transforms complete before outer ones read the tree.
|
||||
const MAX_ITERATIONS = 10
|
||||
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
|
||||
|
||||
// Find all JSX elements with asChild attribute.
|
||||
const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
|
||||
|
||||
for (const jsxElement of jsxElements) {
|
||||
const openingElement = jsxElement.getOpeningElement()
|
||||
const asChildAttr = openingElement.getAttribute("asChild")
|
||||
|
||||
if (!asChildAttr) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parentTagName = openingElement.getTagNameNode().getText()
|
||||
const children = jsxElement.getJsxChildren()
|
||||
|
||||
// Find the first JSX element child (skip whitespace/text).
|
||||
const childElement = children.find(
|
||||
(child) =>
|
||||
child.getKind() === SyntaxKind.JsxElement ||
|
||||
child.getKind() === SyntaxKind.JsxSelfClosingElement
|
||||
// Find all JSX elements with asChild attribute.
|
||||
const asChildElements = jsxElements.filter((el) =>
|
||||
el.getOpeningElement().getAttribute("asChild")
|
||||
)
|
||||
|
||||
if (!childElement) {
|
||||
// No child element found, just remove asChild.
|
||||
asChildAttr.remove()
|
||||
continue
|
||||
if (asChildElements.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
// Get child element info.
|
||||
let childTagName: string
|
||||
let childProps: string
|
||||
let childChildren: string
|
||||
|
||||
if (childElement.getKind() === SyntaxKind.JsxSelfClosingElement) {
|
||||
const selfClosing = childElement.asKindOrThrow(
|
||||
SyntaxKind.JsxSelfClosingElement
|
||||
// Filter to leaf-only: elements with no asChild descendants.
|
||||
const leafElements = asChildElements.filter((el) => {
|
||||
const descendants = el.getDescendantsOfKind(SyntaxKind.JsxElement)
|
||||
return !descendants.some((d) =>
|
||||
d.getOpeningElement().getAttribute("asChild")
|
||||
)
|
||||
childTagName = selfClosing.getTagNameNode().getText()
|
||||
childProps = selfClosing
|
||||
.getAttributes()
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
childChildren = ""
|
||||
} else {
|
||||
const jsxChild = childElement.asKindOrThrow(SyntaxKind.JsxElement)
|
||||
const openingEl = jsxChild.getOpeningElement()
|
||||
childTagName = openingEl.getTagNameNode().getText()
|
||||
childProps = openingEl
|
||||
.getAttributes()
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
// Get the children's text content.
|
||||
childChildren = jsxChild
|
||||
.getJsxChildren()
|
||||
.map((c) => c.getText())
|
||||
.join("")
|
||||
}
|
||||
|
||||
// Determine if we need nativeButton={false}.
|
||||
// Add it when the child element is a non-button element.
|
||||
const needsNativeButton =
|
||||
ELEMENTS_REQUIRING_NATIVE_BUTTON_FALSE.includes(childTagName)
|
||||
|
||||
transformations.push({
|
||||
parentElement: jsxElement,
|
||||
parentTagName,
|
||||
childTagName,
|
||||
childProps,
|
||||
childChildren,
|
||||
needsNativeButton,
|
||||
})
|
||||
}
|
||||
|
||||
// Apply transformations in reverse order to preserve node validity.
|
||||
for (const info of transformations.reverse()) {
|
||||
const openingElement = info.parentElement.getOpeningElement()
|
||||
const closingElement = info.parentElement.getClosingElement()
|
||||
// Collect all transformations first, then apply them in reverse order.
|
||||
// This prevents issues with invalidated nodes when modifying the tree.
|
||||
const transformations: TransformInfo[] = []
|
||||
|
||||
// Get existing attributes (excluding asChild).
|
||||
const existingAttrs = openingElement
|
||||
.getAttributes()
|
||||
.filter((attr) => {
|
||||
if (attr.getKind() === SyntaxKind.JsxAttribute) {
|
||||
const jsxAttr = attr.asKindOrThrow(SyntaxKind.JsxAttribute)
|
||||
return jsxAttr.getNameNode().getText() !== "asChild"
|
||||
}
|
||||
return true
|
||||
for (const jsxElement of leafElements) {
|
||||
const openingElement = jsxElement.getOpeningElement()
|
||||
const asChildAttr = openingElement.getAttribute("asChild")
|
||||
|
||||
if (!asChildAttr) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parentTagName = openingElement.getTagNameNode().getText()
|
||||
const children = jsxElement.getJsxChildren()
|
||||
|
||||
// Find the first JSX element child (skip whitespace/text).
|
||||
const childElement = children.find(
|
||||
(child) =>
|
||||
child.getKind() === SyntaxKind.JsxElement ||
|
||||
child.getKind() === SyntaxKind.JsxSelfClosingElement
|
||||
)
|
||||
|
||||
if (!childElement) {
|
||||
// No child element found, just remove asChild.
|
||||
asChildAttr.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
// Get child element info.
|
||||
let childTagName: string
|
||||
let childProps: string
|
||||
let childChildren: string
|
||||
|
||||
if (childElement.getKind() === SyntaxKind.JsxSelfClosingElement) {
|
||||
const selfClosing = childElement.asKindOrThrow(
|
||||
SyntaxKind.JsxSelfClosingElement
|
||||
)
|
||||
childTagName = selfClosing.getTagNameNode().getText()
|
||||
childProps = selfClosing
|
||||
.getAttributes()
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
childChildren = ""
|
||||
} else {
|
||||
const jsxChild = childElement.asKindOrThrow(SyntaxKind.JsxElement)
|
||||
const openingEl = jsxChild.getOpeningElement()
|
||||
childTagName = openingEl.getTagNameNode().getText()
|
||||
childProps = openingEl
|
||||
.getAttributes()
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
// Get the children's text content.
|
||||
childChildren = jsxChild
|
||||
.getJsxChildren()
|
||||
.map((c) => c.getText())
|
||||
.join("")
|
||||
}
|
||||
|
||||
// Determine if we need nativeButton={false}.
|
||||
// Only add it on Button when the child is a non-button element.
|
||||
const needsNativeButton =
|
||||
parentTagName === "Button" &&
|
||||
ELEMENTS_REQUIRING_NATIVE_BUTTON_FALSE.includes(childTagName)
|
||||
|
||||
transformations.push({
|
||||
parentElement: jsxElement,
|
||||
parentTagName,
|
||||
childTagName,
|
||||
childProps,
|
||||
childChildren,
|
||||
needsNativeButton,
|
||||
})
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
|
||||
// Build the render prop value.
|
||||
const renderValue = info.childProps
|
||||
? `{<${info.childTagName} ${info.childProps} />}`
|
||||
: `{<${info.childTagName} />}`
|
||||
|
||||
// Build new attributes.
|
||||
let newAttrs = existingAttrs ? `${existingAttrs} ` : ""
|
||||
newAttrs += `render=${renderValue}`
|
||||
if (info.needsNativeButton) {
|
||||
newAttrs += ` nativeButton={false}`
|
||||
}
|
||||
|
||||
// Build the new element text.
|
||||
const newChildren = info.childChildren.trim() ? `${info.childChildren}` : ""
|
||||
// Apply transformations in reverse order to preserve node validity.
|
||||
for (const info of transformations.reverse()) {
|
||||
const openingElement = info.parentElement.getOpeningElement()
|
||||
|
||||
const newElementText = `<${info.parentTagName} ${newAttrs}>${newChildren}</${info.parentTagName}>`
|
||||
// Get existing attributes (excluding asChild).
|
||||
const existingAttrs = openingElement
|
||||
.getAttributes()
|
||||
.filter((attr) => {
|
||||
if (attr.getKind() === SyntaxKind.JsxAttribute) {
|
||||
const jsxAttr = attr.asKindOrThrow(SyntaxKind.JsxAttribute)
|
||||
return jsxAttr.getNameNode().getText() !== "asChild"
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map((attr) => attr.getText())
|
||||
.join(" ")
|
||||
|
||||
info.parentElement.replaceWithText(newElementText)
|
||||
// Build the render prop value.
|
||||
const renderValue = info.childProps
|
||||
? `{<${info.childTagName} ${info.childProps} />}`
|
||||
: `{<${info.childTagName} />}`
|
||||
|
||||
// Build new attributes.
|
||||
let newAttrs = existingAttrs ? `${existingAttrs} ` : ""
|
||||
newAttrs += `render=${renderValue}`
|
||||
if (info.needsNativeButton) {
|
||||
newAttrs += ` nativeButton={false}`
|
||||
}
|
||||
|
||||
// Build the new element text.
|
||||
const newChildren = info.childChildren.trim()
|
||||
? `${info.childChildren}`
|
||||
: ""
|
||||
|
||||
const newElementText = `<${info.parentTagName} ${newAttrs}>${newChildren}</${info.parentTagName}>`
|
||||
|
||||
info.parentElement.replaceWithText(newElementText)
|
||||
}
|
||||
}
|
||||
|
||||
return sourceFile
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
registryItemTailwindSchema,
|
||||
} from "@/src/schema"
|
||||
import { Config } from "@/src/utils/get-config"
|
||||
import { getPackageInfo } from "@/src/utils/get-package-info"
|
||||
import { TailwindVersion } from "@/src/utils/get-project-info"
|
||||
import { highlighter } from "@/src/utils/highlighter"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
@@ -21,7 +20,6 @@ export async function updateCssVars(
|
||||
options: {
|
||||
cleanupDefaultNextStyles?: boolean
|
||||
overwriteCssVars?: boolean
|
||||
initIndex?: boolean
|
||||
silent?: boolean
|
||||
tailwindVersion?: TailwindVersion
|
||||
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
@@ -36,7 +34,6 @@ export async function updateCssVars(
|
||||
silent: false,
|
||||
tailwindVersion: "v3",
|
||||
overwriteCssVars: false,
|
||||
initIndex: true,
|
||||
...options,
|
||||
}
|
||||
const cssFilepath = config.resolvedPaths.tailwindCss
|
||||
@@ -56,7 +53,6 @@ export async function updateCssVars(
|
||||
tailwindVersion: options.tailwindVersion,
|
||||
tailwindConfig: options.tailwindConfig,
|
||||
overwriteCssVars: options.overwriteCssVars,
|
||||
initIndex: options.initIndex,
|
||||
})
|
||||
await fs.writeFile(cssFilepath, output, "utf8")
|
||||
cssVarsSpinner.succeed()
|
||||
@@ -71,13 +67,11 @@ export async function transformCssVars(
|
||||
tailwindVersion?: TailwindVersion
|
||||
tailwindConfig?: z.infer<typeof registryItemTailwindSchema>["config"]
|
||||
overwriteCssVars?: boolean
|
||||
initIndex?: boolean
|
||||
} = {
|
||||
cleanupDefaultNextStyles: false,
|
||||
tailwindVersion: "v3",
|
||||
tailwindConfig: undefined,
|
||||
overwriteCssVars: false,
|
||||
initIndex: false,
|
||||
}
|
||||
) {
|
||||
options = {
|
||||
@@ -85,7 +79,6 @@ export async function transformCssVars(
|
||||
tailwindVersion: "v3",
|
||||
tailwindConfig: undefined,
|
||||
overwriteCssVars: false,
|
||||
initIndex: false,
|
||||
...options,
|
||||
}
|
||||
|
||||
@@ -98,18 +91,6 @@ export async function transformCssVars(
|
||||
if (options.tailwindVersion === "v4") {
|
||||
plugins = []
|
||||
|
||||
// Only add tw-animate-css if project does not have tailwindcss-animate
|
||||
if (config.resolvedPaths?.cwd) {
|
||||
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
|
||||
if (
|
||||
!packageInfo?.dependencies?.["tailwindcss-animate"] &&
|
||||
!packageInfo?.devDependencies?.["tailwindcss-animate"] &&
|
||||
options.initIndex
|
||||
) {
|
||||
plugins.push(addCustomImport({ params: "tw-animate-css" }))
|
||||
}
|
||||
}
|
||||
|
||||
plugins.push(addCustomVariant({ params: "dark (&:is(.dark *))" }))
|
||||
|
||||
if (options.cleanupDefaultNextStyles) {
|
||||
@@ -130,12 +111,6 @@ export async function transformCssVars(
|
||||
}
|
||||
}
|
||||
|
||||
if (config.tailwind.cssVariables && options.initIndex) {
|
||||
plugins.push(
|
||||
updateBaseLayerPlugin({ tailwindVersion: options.tailwindVersion })
|
||||
)
|
||||
}
|
||||
|
||||
const result = await postcss(plugins).process(input, {
|
||||
from: undefined,
|
||||
})
|
||||
@@ -151,81 +126,6 @@ export async function transformCssVars(
|
||||
return output
|
||||
}
|
||||
|
||||
function updateBaseLayerPlugin({
|
||||
tailwindVersion,
|
||||
}: {
|
||||
tailwindVersion?: TailwindVersion
|
||||
}) {
|
||||
return {
|
||||
postcssPlugin: "update-base-layer",
|
||||
Once(root: Root) {
|
||||
const requiredRules = [
|
||||
{
|
||||
selector: "*",
|
||||
apply:
|
||||
tailwindVersion === "v4"
|
||||
? "border-border outline-ring/50"
|
||||
: "border-border",
|
||||
},
|
||||
{ selector: "body", apply: "bg-background text-foreground" },
|
||||
]
|
||||
|
||||
let baseLayer = root.nodes.find(
|
||||
(node): node is AtRule =>
|
||||
node.type === "atrule" &&
|
||||
node.name === "layer" &&
|
||||
node.params === "base" &&
|
||||
requiredRules.every(({ selector, apply }) =>
|
||||
node.nodes?.some(
|
||||
(rule): rule is Rule =>
|
||||
rule.type === "rule" &&
|
||||
rule.selector === selector &&
|
||||
rule.nodes.some(
|
||||
(applyRule): applyRule is AtRule =>
|
||||
applyRule.type === "atrule" &&
|
||||
applyRule.name === "apply" &&
|
||||
applyRule.params === apply
|
||||
)
|
||||
)
|
||||
)
|
||||
) as AtRule | undefined
|
||||
|
||||
if (!baseLayer) {
|
||||
baseLayer = postcss.atRule({
|
||||
name: "layer",
|
||||
params: "base",
|
||||
raws: { semicolon: true, between: " ", before: "\n" },
|
||||
})
|
||||
root.append(baseLayer)
|
||||
root.insertBefore(baseLayer, postcss.comment({ text: "---break---" }))
|
||||
}
|
||||
|
||||
requiredRules.forEach(({ selector, apply }) => {
|
||||
const existingRule = baseLayer?.nodes?.find(
|
||||
(node): node is Rule =>
|
||||
node.type === "rule" && node.selector === selector
|
||||
)
|
||||
|
||||
if (!existingRule) {
|
||||
baseLayer?.append(
|
||||
postcss.rule({
|
||||
selector,
|
||||
nodes: [
|
||||
postcss.atRule({
|
||||
name: "apply",
|
||||
params: apply,
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
}),
|
||||
],
|
||||
raws: { semicolon: true, between: " ", before: "\n " },
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function updateCssVarsPlugin(
|
||||
cssVars: z.infer<typeof registryItemCssVarsSchema>
|
||||
) {
|
||||
@@ -662,13 +562,13 @@ function addCustomImport({ params }: { params: string }) {
|
||||
node.type === "atrule" && node.name === "import"
|
||||
)
|
||||
|
||||
// Find custom variant node (to ensure we insert before it)
|
||||
// Find custom variant node (to ensure we insert before it).
|
||||
const customVariantNode = root.nodes.find(
|
||||
(node): node is AtRule =>
|
||||
node.type === "atrule" && node.name === "custom-variant"
|
||||
)
|
||||
|
||||
// Check if our specific import already exists
|
||||
// Check if our specific import already exists.
|
||||
const hasImport = importNodes.some(
|
||||
(node) => node.params.replace(/["']/g, "") === params
|
||||
)
|
||||
@@ -681,18 +581,18 @@ function addCustomImport({ params }: { params: string }) {
|
||||
})
|
||||
|
||||
if (importNodes.length > 0) {
|
||||
// If there are existing imports, add after the last import
|
||||
// If there are existing imports, add after the last import.
|
||||
const lastImport = importNodes[importNodes.length - 1]
|
||||
root.insertAfter(lastImport, importNode)
|
||||
} else if (customVariantNode) {
|
||||
// If no imports but has custom-variant, insert before it
|
||||
// If no imports but has custom-variant, insert before it.
|
||||
root.insertBefore(customVariantNode, importNode)
|
||||
root.insertBefore(
|
||||
customVariantNode,
|
||||
postcss.comment({ text: "---break---" })
|
||||
)
|
||||
} else {
|
||||
// If no imports and no custom-variant, insert at the start
|
||||
// If no imports and no custom-variant, insert at the start.
|
||||
root.prepend(importNode)
|
||||
root.insertAfter(importNode, postcss.comment({ text: "---break---" }))
|
||||
}
|
||||
|
||||
@@ -472,12 +472,23 @@ function processRule(parent: Root | AtRule, selector: string, properties: any) {
|
||||
const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/)
|
||||
if (atRuleMatch) {
|
||||
const [, atRuleName, atRuleParams] = atRuleMatch
|
||||
const atRule = postcss.atRule({
|
||||
name: atRuleName,
|
||||
params: atRuleParams,
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
})
|
||||
rule.append(atRule)
|
||||
|
||||
// Check if this at-rule already exists in the rule.
|
||||
const existingAtRule = rule.nodes?.find(
|
||||
(node): node is AtRule =>
|
||||
node.type === "atrule" &&
|
||||
node.name === atRuleName &&
|
||||
node.params === atRuleParams
|
||||
)
|
||||
|
||||
if (!existingAtRule) {
|
||||
const atRule = postcss.atRule({
|
||||
name: atRuleName,
|
||||
params: atRuleParams,
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
})
|
||||
rule.append(atRule)
|
||||
}
|
||||
}
|
||||
} else if (typeof value === "string") {
|
||||
const decl = postcss.decl({
|
||||
|
||||
@@ -853,6 +853,82 @@ describe("transformCss", () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test("should add base layer styles from registry:style css field", async () => {
|
||||
const input = `@import "tailwindcss";`
|
||||
|
||||
// This is the exact shape from the registry:style index item.
|
||||
const result = await transformCss(input, {
|
||||
'@import "tw-animate-css"': {},
|
||||
'@import "shadcn/tailwind.css"': {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {},
|
||||
},
|
||||
body: {
|
||||
"@apply bg-background text-foreground": {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should not duplicate base layer styles if already present", async () => {
|
||||
const input = `@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
'@import "tw-animate-css"': {},
|
||||
'@import "shadcn/tailwind.css"': {},
|
||||
"@layer base": {
|
||||
"*": {
|
||||
"@apply border-border outline-ring/50": {},
|
||||
},
|
||||
body: {
|
||||
"@apply bg-background text-foreground": {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should replace existing keyframes instead of duplicating", async () => {
|
||||
const input = `@import "tailwindcss";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user