111k

2023 年 6 月 - 新 CLI、样式等更新

完整重写的 CLI,带来新样式、主题选项等功能。

今天我有很多更新要和你分享:

  • 新 CLI - 从零重写 CLI。现在可以添加组件、依赖并配置导入路径。
  • 主题 - 在使用 CSS 变量或 Tailwind CSS 工具类之间选择主题方案。
  • 基础色 - 为项目配置基础色。它会用于生成组件的默认调色板。
  • React Server Components - 可选择不使用 React Server Components。CLI 会自动添加或移除 use client 指令。
  • 样式 - 引入一个名为 Style 的新概念。样式会自带一套组件、动画、图标等内容。
  • 退出动画 - 为所有组件添加了退出动画。
  • 其他更新 - 新的 icon 按钮尺寸、更新后的 sheet 组件等。
  • 更新你的项目 - 如何更新项目以获取最新变更。

新 CLI

过去几周我一直在开发一个新的 CLI。它是一次彻底重写,带来了很多新功能和改进。

init

pnpm dlx shadcn@latest init

运行 init 命令时,系统会询问你一些问题来配置 components.json

Which style would you like to use? › Default
Which color would you like to use as base color? › Slate
Where is your global CSS file? › › app/globals.css
Do you want to use CSS variables for colors? › no / yes
Where is your tailwind.config.js located? › tailwind.config.js
Configure the import alias for components: › @/components
Configure the import alias for utils: › @/lib/utils
Are you using React Server Components? › no / yes

这个文件包含了组件的所有信息:安装位置、导入路径、样式方式等等。

你可以用这个文件来修改组件的导入路径、设置基础色,或者更改样式方法。

components.json
{
  "style": "default",
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "rsc": false,
  "aliases": {
    "utils": "~/lib/utils",
    "components": "~/components"
  }
}

这意味着你现在可以在任何目录结构中使用 CLI,包括 srcapp 目录。

add

pnpm dlx shadcn@latest add

add 命令现在更强大了。你现在不仅可以添加 UI 组件,还可以导入更复杂的组件(即将推出)。

CLI 会自动解析所有组件和依赖,依据你的自定义配置进行格式化,并把它们添加到项目中。

diff(实验性)

pnpm dlx shadcn diff

我们还引入了新的 diff 命令,帮助你跟踪上游更新。

你可以使用这个命令查看上游仓库发生了什么变化,并据此更新你的项目。

Run the diff command to get a list of components that have updates available:

pnpm dlx shadcn diff
以下组件有可用更新:
- button
  - /path/to/my-app/components/ui/button.tsx
- toast
  - /path/to/my-app/components/ui/use-toast.ts
  - /path/to/my-app/components/ui/toaster.tsx

然后运行 diff [component] 查看具体变更:

pnpm dlx shadcn diff alert
const alertVariants = cva(
- "relative w-full rounded-lg border",
+ "relative w-full pl-12 rounded-lg border"
)

使用 CSS 变量或 Tailwind 颜色进行主题化

你可以在使用 CSS 变量或 Tailwind CSS 工具类之间选择主题方案。

当你添加新组件时,CLI 会根据你的 components.json 配置自动使用正确的主题方式。

Utility classes

<div className="bg-zinc-950 dark:bg-white" />

如果要使用工具类进行主题化,请在 components.json 文件中把 tailwind.cssVariables 设为 false

components.json
{
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": false
  }
}

CSS Variables

<div className="bg-background text-foreground" />

如果要使用 CSS 变量进行主题化,请在 components.json 文件中把 tailwind.cssVariables 设为 true

components.json
{
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  }
}

基础色

你现在可以为项目配置基础色。它会用于生成组件的默认调色板。

components.json
{
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": false
  }
}

可在 grayneutralslatestonezinc 之间选择。

如果你把 cssVariables 设为 true,我们会在你的 globals.css 文件中把基础颜色设置为 CSS 变量。如果你把 cssVariables 设为 false,我们就会把 Tailwind CSS 工具类直接内联到组件中。


React Server Components

如果你使用的框架不支持 React Server Components,现在可以通过把 rsc 设为 false 来退出。添加组件时,我们会自动添加或移除 use client 指令。

components.json
{
  "rsc": false
}

样式

我们正在引入一个名为 Style 的新概念。

你可以把 style 理解为视觉基础:形状、图标、动画和排版。 一种 style 会自带自己的组件、动画、图标等内容。

我们提供两种样式:defaultnew-york(后续还会有更多)。

Default vs New York style

default 样式就是你熟悉的那个样式。从项目开始我们一直在使用它。它使用 lucide-react 作为图标,并使用 tailwindcss-animate 作为动画。

new-york 是一种新样式。它包含更小的按钮、带阴影的卡片,以及来自 Radix Icons 的一套新图标。

运行 init 命令时,系统会询问你想使用哪种样式。这个选择会保存到你的 components.json 文件中。

components.json
{
  "style": "new-york"
}

主题化

先以某种样式作为基础,然后通过 CSS 变量或 Tailwind CSS 工具类进行主题化,就可以完全改变组件的外观。

Style with theming

退出动画

我为所有组件都添加了退出动画。点击下面的 combobox 可以看到细微的退出动画。

"use client"

import * as React from "react"

这些动画可以通过工具类自定义。


其他更新

Button

  • 新增了一个 icon 按钮尺寸:
import { CircleFadingArrowUpIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Sheet

  • position 重命名为 side,以与其他元素保持一致。
"use client"

import { Button } from "@/components/ui/button"
  • 移除了 size 属性。请使用 className="w-[200px] md:w-[450px]" 来实现响应式尺寸。

更新你的项目

由于我们采用复制粘贴的方式,你需要手动更新项目才能获得最新变更。

添加 components.json

在根目录创建一个 components.json 文件:

components.json
{
  "style": "default",
  "rsc": true,
  "tailwind": {
    "config": "tailwind.config.js",
    "css": "app/globals.css",
    "baseColor": "slate",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

请更新 tailwind.cssaliases 的值,以匹配你的项目结构。

Button

buttonVariants 中添加 icon 尺寸:

components/ui/button.tsx
const buttonVariants = cva({
  variants: {
    size: {
      default: "h-10 px-4 py-2",
      sm: "h-9 rounded-md px-3",
      lg: "h-11 rounded-md px-8",
      icon: "h-10 w-10",
    },
  },
})

Sheet

  1. sheet.tsx 的内容替换为以下内容:
components/ui/sheet.tsx
"use client"
 
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
 
import { cn } from "@/lib/utils"
 
const Sheet = SheetPrimitive.Root
 
const SheetTrigger = SheetPrimitive.Trigger
 
const SheetClose = SheetPrimitive.Close
 
const SheetPortal = ({
  className,
  ...props
}: SheetPrimitive.DialogPortalProps) => (
  <SheetPrimitive.Portal className={cn(className)} {...props} />
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
 
const SheetOverlay = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
      className
    )}
    {...props}
    ref={ref}
  />
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
 
const sheetVariants = cva(
  "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
  {
    variants: {
      side: {
        top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
        bottom:
          "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
        left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
        right:
          "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
      },
    },
    defaultVariants: {
      side: "right",
    },
  }
)
 
interface SheetContentProps
  extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
    VariantProps<typeof sheetVariants> {}
 
const SheetContent = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Content>,
  SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
  <SheetPortal>
    <SheetOverlay />
    <SheetPrimitive.Content
      ref={ref}
      className={cn(sheetVariants({ side }), className)}
      {...props}
    >
      {children}
      <SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-none disabled:pointer-events-none data-[state=open]:bg-secondary">
        <X className="h-4 w-4" />
        <span className="sr-only">Close</span>
      </SheetPrimitive.Close>
    </SheetPrimitive.Content>
  </SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
 
const SheetHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
SheetHeader.displayName = "SheetHeader"
 
const SheetFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
SheetFooter.displayName = "SheetFooter"
 
const SheetTitle = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold text-foreground", className)}
    {...props}
  />
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
 
const SheetDescription = React.forwardRef<
  React.ElementRef<typeof SheetPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
  <SheetPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
 
export {
  Sheet,
  SheetTrigger,
  SheetClose,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
}
  1. Rename position to side
- <Sheet position="right" />
+ <Sheet side="right" />

感谢

我要感谢一直在使用这个项目、提供反馈并参与贡献的每一个人。我非常感激。谢谢你们。