AniBTUI
✦ 组件@anibt/progressregistry JSON

Progress

下沉的轨道和带光泽的填充:蓝色是进行中,糖果粉是你自己的目标,薄荷是已完成。

安装

npx shadcn@latest add @anibt/progress

第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。

手动安装
  1. 1. 先装主题组件只读 @anibt/theme 的 token 和配方,没有主题就没有样子。
    npx shadcn@latest add @anibt/theme
  2. 2. 安装依赖
    npm install class-variance-authority radix-ui
  3. 3. 把下面的文件放进项目
    components/ui/progress.tsx
    "use client"
    
    import * as React from "react"
    import { cva, type VariantProps } from "class-variance-authority"
    import { Progress as ProgressPrimitive } from "radix-ui"
    
    import { cn } from "@/lib/utils"
    
    /**
     * Progress — the AniBT 「贴纸手账」 bar.
     *
     * A sunk track with a 1px edge and a glossy fill. Blue is A's colour for
     * "in progress", so the fill is `water` by default; `tone="candy"` is for a
     * quota or a goal the reader is filling (pink = the reader's own action) and
     * `tone="ok"` for work that is done. `value={null}` is indeterminate: a short
     * fill slides along the track (it holds still under reduced motion).
     *
     * `<Progress value={40} />` is shadcn's shape. Children turn it into Base UI's
     * shape — a label and a figure above the track:
     *
     *   <Progress value={3} max={5}>
     *     <ProgressLabel>同步目标</ProgressLabel>
     *     <ProgressValue />            → 3 / 5 (or a percentage, see `format`)
     *   </Progress>
     */
    const trackVariants = cva(
      "relative w-full overflow-hidden rounded-full bg-muted shadow-[inset_0_0_0_1px_var(--border)]",
      {
        variants: {
          size: {
            default: "h-2.5",
            compact: "h-1.5",
            lg: "h-3.5",
          },
        },
        defaultVariants: {
          size: "default",
        },
      }
    )
    
    const indicatorVariants = cva(
      "h-full w-full rounded-full transition-transform duration-500 ease-anibt-out motion-reduce:transition-none",
      {
        variants: {
          tone: {
            water: "anibt-water shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]",
            candy: "anibt-candy shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]",
            ok: "bg-anibt-ok-fill shadow-[inset_0_1px_0_rgba(255,255,255,0.35)]",
          },
        },
        defaultVariants: {
          tone: "water",
        },
      }
    )
    
    type ProgressContextValue = { value: number | null; max: number }
    
    const ProgressContext = React.createContext<ProgressContextValue>({ value: null, max: 100 })
    
    type ProgressProps = Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, "value" | "max"> &
      VariantProps<typeof trackVariants> &
      VariantProps<typeof indicatorVariants> & {
        /** `null` (or leaving it out) is indeterminate. */
        value?: number | null
        max?: number
        /** Classes for the track when the progress has children (label row). */
        trackClassName?: string
      }
    
    function Progress({
      className,
      trackClassName,
      value = null,
      max = 100,
      size,
      tone,
      children,
      ...props
    }: ProgressProps) {
      const track = (
        <ProgressPrimitive.Root
          data-slot="progress"
          data-tone={tone ?? "water"}
          value={value}
          max={max}
          className={cn(trackVariants({ size }), children ? trackClassName : className)}
          {...props}
        >
          <ProgressIndicator tone={tone} />
        </ProgressPrimitive.Root>
      )
    
      if (!children) {
        return <ProgressContext.Provider value={{ value, max }}>{track}</ProgressContext.Provider>
      }
    
      return (
        <ProgressContext.Provider value={{ value, max }}>
          <div data-slot="progress-group" className={cn("flex w-full flex-wrap items-baseline gap-x-3 gap-y-1.5", className)}>
            {children}
            {track}
          </div>
        </ProgressContext.Provider>
      )
    }
    
    /** The fill. Rendered by `Progress`; exported for shadcn parity. */
    function ProgressIndicator({
      className,
      tone,
      style,
      ...props
    }: React.ComponentProps<typeof ProgressPrimitive.Indicator> & VariantProps<typeof indicatorVariants>) {
      const { value, max } = React.useContext(ProgressContext)
      const indeterminate = value === null
      const percent = indeterminate ? 0 : Math.min(100, Math.max(0, (value / max) * 100))
    
      return (
        <ProgressPrimitive.Indicator
          data-slot="progress-indicator"
          className={cn(
            indicatorVariants({ tone }),
            indeterminate && "w-2/5 animate-anibt-indeterminate motion-reduce:animate-none",
            className
          )}
          style={indeterminate ? style : { transform: `translateX(-${100 - percent}%)`, ...style }}
          {...props}
        />
      )
    }
    
    /** The track on its own, for a hand-built layout (shadcn `base-*` parity). */
    function ProgressTrack({ className, size, ...props }: React.ComponentProps<"div"> & VariantProps<typeof trackVariants>) {
      return <div data-slot="progress-track" className={cn(trackVariants({ size }), className)} {...props} />
    }
    
    /** What is progressing: 13/600, the field-label voice. */
    function ProgressLabel({ className, ...props }: React.ComponentProps<"span">) {
      return (
        <span
          data-slot="progress-label"
          className={cn("text-[13px] font-semibold text-anibt-ink2", className)}
          {...props}
        />
      )
    }
    
    /**
     * The figure at the right end of the label row, in the round face. Without
     * children it prints the value: `3 / 5`, or `60%` when `max` is 100 or
     * `format="percent"`.
     */
    function ProgressValue({
      className,
      children,
      format,
      ...props
    }: React.ComponentProps<"span"> & { format?: "fraction" | "percent" }) {
      const { value, max } = React.useContext(ProgressContext)
      const asPercent = format ? format === "percent" : max === 100
      const text =
        value === null ? "—" : asPercent ? `${Math.round((value / max) * 100)}%` : `${value} / ${max}`
      return (
        <span
          data-slot="progress-value"
          className={cn("anibt-num ml-auto text-[12px] font-semibold text-muted-foreground", className)}
          {...props}
        >
          {children ?? text}
        </span>
      )
    }
    
    export { Progress, ProgressIndicator, ProgressTrack, ProgressLabel, ProgressValue }
    export type { ProgressProps }

用法

用法
import { Progress } from "@/components/ui/progress"

<Progress value={62} aria-label="上传进度" />

一种颜色一种职责:tone="water"(默认)是蓝色,表示进行中;tone="candy" 给读者自己在填的目标(本月发布、追番进度);tone="ok" 表示已经完成。size 有 compact(6 高,放在行里)、default(10 高)和 lg(14 高)。

标签与数值

同步到外站3 / 5
本季追番进度72%
正在解析种子请稍候

有子元素时,进度条变成 Base UI 的样子:上面一行是 ProgressLabel 和 ProgressValue,下面是轨道。ProgressValue 不写内容时自己算:max 是 100 时显示百分比,否则显示 3 / 5;format 可以强制其中一种。

value={null}(或不传)是不确定进度:一小段填充沿着轨道来回滑动,减少动态效果时停住。

属性

Progress
valuenumber | null默认 null当前值;null 为不确定进度
maxnumber默认 100最大值
tone"water" | "candy" | "ok"默认 "water"进行中 / 你的目标 / 已完成
size"compact" | "default" | "lg"默认 "default"6 / 10 / 14 高
trackClassNamestring默认 —有标签行时,轨道自己的 class
ProgressValue
format"fraction" | "percent"默认 —不写时 max 为 100 显示百分比,否则显示分数

为了和 shadcn 对齐,还导出 ProgressIndicator、ProgressTrack、ProgressLabel。