Skip to content

Commit

Permalink
(2/n) shadcn: css vars for tailwind v4 (#6487)
Browse files Browse the repository at this point in the history
* feat(shadcn): add tailwind version detection

* chore: changeset

* feat(shadcn): css vars for tailwind v4

* fix(shadcn): handle color space

* fix(shadcn): add oklch support

* feat(shadcn): handle single quote

* chore: add changeset
  • Loading branch information
shadcn authored Jan 30, 2025
1 parent 8f6a64f commit 5ef2bc5
Show file tree
Hide file tree
Showing 7 changed files with 565 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-houses-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn": minor
---

add theme vars support
11 changes: 11 additions & 0 deletions packages/shadcn/src/utils/add-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
workspaceConfigSchema,
type Config,
} from "@/src/utils/get-config"
import { getProjectTailwindVersionFromConfig } from "@/src/utils/get-project-info"
import { handleError } from "@/src/utils/handle-error"
import { logger } from "@/src/utils/logger"
import { spinner } from "@/src/utils/spinner"
Expand Down Expand Up @@ -74,12 +75,16 @@ async function addProjectComponents(
}
registrySpinner?.succeed()

const tailwindVersion = await getProjectTailwindVersionFromConfig(config)

await updateTailwindConfig(tree.tailwind?.config, config, {
silent: options.silent,
tailwindVersion,
})
await updateCssVars(tree.cssVars, config, {
cleanupDefaultNextStyles: options.isNewProject,
silent: options.silent,
tailwindVersion,
})

await updateDependencies(tree.dependencies, config, {
Expand Down Expand Up @@ -143,6 +148,10 @@ async function addWorkspaceComponents(
? workspaceConfig.ui
: config

const tailwindVersion = await getProjectTailwindVersionFromConfig(
targetConfig
)

const workspaceRoot = findCommonRoot(
config.resolvedPaths.cwd,
targetConfig.resolvedPaths.ui
Expand All @@ -155,6 +164,7 @@ async function addWorkspaceComponents(
if (component.tailwind?.config) {
await updateTailwindConfig(component.tailwind?.config, targetConfig, {
silent: true,
tailwindVersion,
})
filesUpdated.push(
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindConfig)
Expand All @@ -165,6 +175,7 @@ async function addWorkspaceComponents(
if (component.cssVars) {
await updateCssVars(component.cssVars, targetConfig, {
silent: true,
tailwindVersion,
})
filesUpdated.push(
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss)
Expand Down
26 changes: 24 additions & 2 deletions packages/shadcn/src/utils/get-project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import fs from "fs-extra"
import { loadConfig } from "tsconfig-paths"
import { z } from "zod"

export type TailwindVersion = "v3" | "v4" | null

type ProjectInfo = {
framework: Framework
isSrcDir: boolean
isRSC: boolean
isTsx: boolean
tailwindConfigFile: string | null
tailwindCssFile: string | null
tailwindVersion: "v3" | "v4" | null
tailwindVersion: TailwindVersion
aliasPrefix: string | null
}

Expand Down Expand Up @@ -168,7 +170,11 @@ export async function getTailwindCssFile(cwd: string) {
tailwindVersion === "v4" ? `@import "tailwindcss"` : "@tailwind base"
for (const file of files) {
const contents = await fs.readFile(path.resolve(cwd, file), "utf8")
if (contents.includes(needle)) {
if (
contents.includes(`@import "tailwindcss"`) ||
contents.includes(`@import 'tailwindcss'`) ||
contents.includes(`@tailwind base`)
) {
return file
}
}
Expand Down Expand Up @@ -300,3 +306,19 @@ export async function getProjectConfig(

return await resolveConfigPaths(cwd, config)
}

export async function getProjectTailwindVersionFromConfig(
config: Config
): Promise<TailwindVersion> {
if (!config.resolvedPaths.cwd) {
return "v3"
}

const projectInfo = await getProjectInfo(config.resolvedPaths.cwd)

if (!projectInfo?.tailwindVersion) {
return null
}

return projectInfo.tailwindVersion
}
142 changes: 140 additions & 2 deletions packages/shadcn/src/utils/updaters/update-css-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { promises as fs } from "fs"
import path from "path"
import { registryItemCssVarsSchema } from "@/src/registry/schema"
import { Config } from "@/src/utils/get-config"
import { TailwindVersion, getProjectInfo } from "@/src/utils/get-project-info"
import { highlighter } from "@/src/utils/highlighter"
import { spinner } from "@/src/utils/spinner"
import postcss from "postcss"
Expand All @@ -16,6 +17,7 @@ export async function updateCssVars(
options: {
cleanupDefaultNextStyles?: boolean
silent?: boolean
tailwindVersion?: TailwindVersion
}
) {
if (
Expand All @@ -29,6 +31,7 @@ export async function updateCssVars(
options = {
cleanupDefaultNextStyles: false,
silent: false,
tailwindVersion: "v3",
...options,
}
const cssFilepath = config.resolvedPaths.tailwindCss
Expand All @@ -45,6 +48,7 @@ export async function updateCssVars(
const raw = await fs.readFile(cssFilepath, "utf8")
let output = await transformCssVars(raw, cssVars, config, {
cleanupDefaultNextStyles: options.cleanupDefaultNextStyles,
tailwindVersion: options.tailwindVersion,
})
await fs.writeFile(cssFilepath, output, "utf8")
cssVarsSpinner.succeed()
Expand All @@ -56,21 +60,32 @@ export async function transformCssVars(
config: Config,
options: {
cleanupDefaultNextStyles?: boolean
tailwindVersion?: TailwindVersion
} = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
}
) {
options = {
cleanupDefaultNextStyles: false,
tailwindVersion: "v3",
...options,
}

const plugins = [updateCssVarsPlugin(cssVars)]
let plugins = [updateCssVarsPlugin(cssVars)]

if (options.tailwindVersion === "v4") {
plugins = [
addCustomVariant({ params: "dark (&:is(.dark *))" }),
updateCssVarsPluginV4(cssVars),
updateThemePlugin(cssVars),
]
}

if (options.cleanupDefaultNextStyles) {
plugins.push(cleanupDefaultNextStylesPlugin())
}

// Only add the base layer plugin if we're using css variables.
if (config.tailwind.cssVariables) {
plugins.push(updateBaseLayerPlugin())
}
Expand Down Expand Up @@ -298,3 +313,126 @@ function addOrUpdateVars(
existingDecl ? existingDecl.replaceWith(newDecl) : ruleNode?.append(newDecl)
})
}

function updateCssVarsPluginV4(
cssVars: z.infer<typeof registryItemCssVarsSchema>
) {
return {
postcssPlugin: "update-css-vars-v4",
Once(root: Root) {
Object.entries(cssVars).forEach(([key, vars]) => {
const selector = key === "light" ? ":root" : `.${key}`

let ruleNode = root.nodes?.find(
(node): node is Rule =>
node.type === "rule" && node.selector === selector
)

if (!ruleNode) {
ruleNode = postcss.rule({
selector,
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" },
})
root.append(ruleNode)
}

Object.entries(vars).forEach(([key, value]) => {
const prop = `--${key.replace(/^--/, "")}`

if (
!value.startsWith("hsl") &&
!value.startsWith("rgb") &&
!value.startsWith("#") &&
!value.startsWith("oklch")
) {
value = `hsl(${value})`
}

const newDecl = postcss.decl({
prop,
value,
raws: { semicolon: true },
})
const existingDecl = ruleNode?.nodes.find(
(node): node is postcss.Declaration =>
node.type === "decl" && node.prop === prop
)
existingDecl
? existingDecl.replaceWith(newDecl)
: ruleNode?.append(newDecl)
})
})
},
}
}

function updateThemePlugin(cssVars: z.infer<typeof registryItemCssVarsSchema>) {
return {
postcssPlugin: "update-theme",
Once(root: Root) {
let themeNode = root.nodes.find(
(node): node is AtRule =>
node.type === "atrule" &&
node.name === "theme" &&
node.params === "inline"
)

if (!themeNode) {
themeNode = postcss.atRule({
name: "theme",
params: "inline",
nodes: [],
raws: { semicolon: true, between: " ", before: "\n" },
})
root.append(themeNode)
}

// Find unique color names from light and dark.
const colors = Array.from(
new Set(
Object.keys(cssVars).flatMap((key) =>
Object.keys(cssVars[key as keyof typeof cssVars] || {})
)
)
)

for (const color of colors) {
const colorVar = postcss.decl({
prop: `--color-${color.replace(/^--/, "")}`,
value: `var(--${color})`,
raws: { semicolon: true },
})
const existingDecl = themeNode?.nodes?.find(
(node): node is postcss.Declaration =>
node.type === "decl" && node.prop === colorVar.prop
)
if (!existingDecl) {
themeNode?.append(colorVar)
}
}
},
}
}

function addCustomVariant({ params }: { params: string }) {
return {
postcssPlugin: "add-custom-variant",
Once(root: Root) {
const customVariant = root.nodes.find(
(node): node is AtRule =>
node.type === "atrule" && node.name === "custom-variant"
)
if (!customVariant) {
root.insertAfter(
root.nodes[0],
postcss.atRule({
name: "custom-variant",
params,
raws: { semicolon: true, before: "\n" },
})
)
}
},
}
}
8 changes: 8 additions & 0 deletions packages/shadcn/src/utils/updaters/update-tailwind-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { tmpdir } from "os"
import path from "path"
import { registryItemTailwindSchema } from "@/src/registry/schema"
import { Config } from "@/src/utils/get-config"
import { TailwindVersion } from "@/src/utils/get-project-info"
import { highlighter } from "@/src/utils/highlighter"
import { spinner } from "@/src/utils/spinner"
import deepmerge from "deepmerge"
Expand Down Expand Up @@ -32,6 +33,7 @@ export async function updateTailwindConfig(
config: Config,
options: {
silent?: boolean
tailwindVersion?: TailwindVersion
}
) {
if (!tailwindConfig) {
Expand All @@ -40,9 +42,15 @@ export async function updateTailwindConfig(

options = {
silent: false,
tailwindVersion: "v3",
...options,
}

// No tailwind config in v4.
if (options.tailwindVersion === "v4") {
return
}

const tailwindFileRelativePath = path.relative(
config.resolvedPaths.cwd,
config.resolvedPaths.tailwindConfig
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@import "tailwindcss";
@import 'tailwindcss';
Loading

0 comments on commit 5ef2bc5

Please sign in to comment.