mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-25 05:35:48 +00:00
Merge pull request #9592 from shadcn-ui/shadcn/fix-base-layer-handling
feat: update handling of base styles
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -204,7 +204,7 @@ export const create = new Command()
|
||||
rtl: opts.rtl,
|
||||
template,
|
||||
baseColor,
|
||||
baseStyle: false,
|
||||
installStyleIndex: false,
|
||||
registryBaseConfig,
|
||||
skipPreflight: false,
|
||||
})
|
||||
@@ -218,7 +218,6 @@ export const create = new Command()
|
||||
components.push("direction")
|
||||
}
|
||||
await addComponents(components, config, {
|
||||
baseStyle: false,
|
||||
silent: true,
|
||||
overwrite: true,
|
||||
})
|
||||
|
||||
@@ -104,7 +104,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(),
|
||||
})
|
||||
@@ -157,6 +157,7 @@ export const init = new Command()
|
||||
isNewProject: false,
|
||||
components,
|
||||
...opts,
|
||||
installStyleIndex: opts.baseStyle,
|
||||
})
|
||||
|
||||
await loadEnvFiles(options.cwd)
|
||||
@@ -228,8 +229,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") {
|
||||
@@ -238,14 +239,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"
|
||||
}
|
||||
|
||||
@@ -326,7 +326,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 ?? []),
|
||||
]
|
||||
|
||||
@@ -389,7 +389,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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