Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/migrate-base-color.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn": minor
---

add `npx shadcn migrate base-color` to switch a project's base color.
51 changes: 38 additions & 13 deletions apps/v4/content/docs/(root)/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,12 @@ npx shadcn@latest migrate [migration]

**Available Migrations**

| Migration | Description |
| --------- | ------------------------------------------------------- |
| `icons` | Migrate your UI components to a different icon library. |
| `radix` | Migrate to radix-ui. |
| `rtl` | Migrate your components to support RTL (right-to-left). |
| Migration | Description |
| ------------ | ------------------------------------------------------- |
| `icons` | Migrate your UI components to a different icon library. |
| `base-color` | Migrate your theme to a different base color. |
| `radix` | Migrate to radix-ui. |
| `rtl` | Migrate your components to support RTL (right-to-left). |

**Options**

Expand All @@ -422,16 +423,16 @@ Usage: shadcn migrate [options] [migration] [path]
run a migration.

Arguments:
migration the migration to run.
path optional path or glob pattern to migrate.
migration the migration to run.
path optional path or glob pattern to migrate.

Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-l, --list list all migrations. (default: false)
-y, --yes skip confirmation prompt. (default: false)
-f, --from <library> the icon library to migrate from (icons migration only).
-t, --to <library> the icon library to migrate to (icons migration only).
-h, --help display help for command
-c, --cwd <cwd> the working directory. defaults to the current directory.
-l, --list list all migrations. (default: false)
-y, --yes skip confirmation prompt. (default: false)
-f, --from <name> the base color or icon library to migrate from.
-t, --to <name> the base color or icon library to migrate to.
-h, --help display help for command
```

---
Expand Down Expand Up @@ -472,6 +473,30 @@ Icons without an equivalent in the target library are left untouched and reporte

---

### migrate base-color

The `base-color` migration switches your theme from one base color to another.

```bash
npx shadcn@latest migrate base-color
```

This will prompt you for the source and target base colors, rewrite the theme CSS variables (the CSS file configured by `tailwind.css` in your `components.json`) and update `baseColor` so future `npx shadcn add` installs use the new base color.

The following base colors are supported: `neutral`, `zinc`, `stone`, `mauve`, `olive`, `mist` and `taupe`.

**Non-interactive**

Use `--to` to migrate from the current `baseColor` in your `components.json`, or pass both `--from` and `--to` explicitly.

```bash
npx shadcn@latest migrate base-color --to zinc --yes
```

Theme tokens that no longer match the source base color are left untouched and reported at the end of the migration.

---

### migrate rtl

The `rtl` migration transforms your components to support RTL (right-to-left) languages.
Expand Down
22 changes: 16 additions & 6 deletions packages/shadcn/src/commands/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from "path"
import { migrateBaseColor } from "@/src/migrations/migrate-base-color"
import { migrateIcons } from "@/src/migrations/migrate-icons"
import { migrateRadix } from "@/src/migrations/migrate-radix"
import { migrateRtl } from "@/src/migrations/migrate-rtl"
Expand All @@ -14,6 +15,10 @@ export const migrations = [
name: "icons",
description: "migrate your ui components to a different icon library.",
},
{
name: "base-color",
description: "migrate your theme to a different base color.",
},
{
name: "radix",
description: "migrate to radix-ui.",
Expand Down Expand Up @@ -57,13 +62,10 @@ export const migrate = new Command()
.option("-l, --list", "list all migrations.", false)
.option("-y, --yes", "skip confirmation prompt.", false)
.option(
"-f, --from <library>",
"the icon library to migrate from (icons migration only)."
)
.option(
"-t, --to <library>",
"the icon library to migrate to (icons migration only)."
"-f, --from <name>",
"the base color or icon library to migrate from."
)
.option("-t, --to <name>", "the base color or icon library to migrate to.")
.action(async (migration, migratePath, opts) => {
try {
const options = migrateOptionsSchema.parse({
Expand Down Expand Up @@ -116,6 +118,14 @@ export const migrate = new Command()
})
}

if (options.migration === "base-color") {
await migrateBaseColor(config, {
from: options.from,
to: options.to,
yes: options.yes,
})
}

if (options.migration === "radix") {
await migrateRadix(config, { yes: options.yes, path: options.path })
}
Expand Down
178 changes: 178 additions & 0 deletions packages/shadcn/src/migrations/migrate-base-color.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Config } from "@/src/utils/get-config"
import { transformCssVars } from "@/src/utils/updaters/update-css-vars"
import { describe, expect, it } from "vitest"

import { getBaseColorMigration } from "./migrate-base-color"

const SOURCE = {
light: {
background: "oklch(1 0 0)",
ring: "oklch(0.7 0.01 286)",
primary: "oklch(0.2 0 0)",
border: "oklch(0.9 0.01 286)",
},
dark: {
background: "oklch(0.14 0 0)",
ring: "oklch(0.55 0.02 286)",
},
}

const TARGET = {
light: {
background: "oklch(1 0 0)",
ring: "oklch(0.708 0 0)",
primary: "oklch(0.205 0 0)",
border: "oklch(0.922 0 0)",
},
dark: {
background: "oklch(0.14 0 0)",
ring: "oklch(0.556 0 0)",
},
}

describe("getBaseColorMigration", () => {
it("replaces tokens that still hold the source base color value", () => {
const css = `@import "tailwindcss";

:root {
--background: oklch(1 0 0);
--ring: oklch(0.7 0.01 286);
--primary: oklch(0.2 0 0);
--border: oklch(0.9 0.01 286);
}

.dark {
--background: oklch(0.14 0 0);
--ring: oklch(0.55 0.02 286);
}`

const { cssVars, skipped } = getBaseColorMigration(
css,
SOURCE,
TARGET,
"v4"
)

expect(cssVars.light).toEqual({
ring: "oklch(0.708 0 0)",
primary: "oklch(0.205 0 0)",
border: "oklch(0.922 0 0)",
})
expect(cssVars.dark).toEqual({ ring: "oklch(0.556 0 0)" })
expect(skipped).toEqual([])
})

it("keeps and reports tokens that no longer match the source", () => {
const css = `:root {
--ring: oklch(0.7 0.01 286);
--primary: oklch(0.55 0.22 260);
}`

const { cssVars, skipped } = getBaseColorMigration(
css,
SOURCE,
TARGET,
"v4"
)

expect(cssVars.light.ring).toBe("oklch(0.708 0 0)")
expect(cssVars.light.primary).toBeUndefined()
expect(skipped).toContainEqual({
token: "--primary",
reason: "does not match the source base color",
})
})

it("does not touch or report tokens that are equal across base colors", () => {
const css = `:root {
--background: oklch(1 0 0);
--ring: oklch(0.7 0.01 286);
}`

const { cssVars, skipped } = getBaseColorMigration(
css,
SOURCE,
TARGET,
"v4"
)

expect(cssVars.light.background).toBeUndefined()
expect(skipped.map((entry) => entry.token)).not.toContain("--background")
})

it("reports tokens that are missing from the CSS", () => {
const css = `:root {
--ring: oklch(0.7 0.01 286);
}`

const { skipped } = getBaseColorMigration(css, SOURCE, TARGET, "v4")

expect(skipped).toContainEqual({
token: "--border",
reason: "not found in your CSS",
})
})

it("wraps local hsl channels before comparing in v4", () => {
const source = { light: { border: "0 0% 90%" } }
const target = { light: { border: "0 0% 92%" } }
const css = `:root {
--border: hsl(0 0% 90%);
}`

const { cssVars, skipped } = getBaseColorMigration(
css,
source,
target,
"v4"
)

expect(cssVars.light.border).toBe("0 0% 92%")
expect(skipped).toEqual([])
})
})

describe("getBaseColorMigration + transformCssVars", () => {
it("writes the computed subset into the theme CSS for v4", async () => {
const source = {
light: {
ring: "oklch(0.7 0.01 286)",
sidebar: "oklch(0.98 0.01 286)",
},
dark: { ring: "oklch(0.55 0.02 286)" },
}
const target = {
light: {
ring: "oklch(0.708 0 0)",
sidebar: "oklch(0.985 0 0)",
},
dark: { ring: "oklch(0.556 0 0)" },
}
const css = `@import "tailwindcss";

:root {
--ring: oklch(0.7 0.01 286);
--sidebar: oklch(0.98 0.01 286);
--primary: oklch(0.55 0.22 260);
}

.dark {
--ring: oklch(0.55 0.02 286);
}`

const { cssVars } = getBaseColorMigration(css, source, target, "v4")
const output = await transformCssVars(css, cssVars, {} as Config, {
tailwindVersion: "v4",
overwriteCssVars: true,
})

// Stock tokens are rewritten to the target base color in both modes.
expect(output).toContain("--ring: oklch(0.708 0 0)")
expect(output).toContain("--sidebar: oklch(0.985 0 0)")
expect(output).toContain("--ring: oklch(0.556 0 0)")
// Tokens outside the computed subset are left untouched.
expect(output).toContain("--primary: oklch(0.55 0.22 260)")
// The @theme inline color mappings are (re)generated.
expect(output).toContain("@theme inline")
})
})
Loading
Loading