meter

PreviousNext

meter

Docs
intentuiui

Preview

Loading preview…
components/ui/meter.tsx
"use client"

import { createContext, use } from "react"
import {
  Meter as PrimitiveMeter,
  type MeterProps as PrimitiveMeterProps,
  type MeterRenderProps as PrimitiveMeterRenderProps,
} from "react-aria-components"
import { twMerge } from "tailwind-merge"
import { cx } from "@/lib/primitive"

interface MeterRenderProps extends PrimitiveMeterRenderProps {
  color?: string
}

const MeterContext = createContext<MeterRenderProps | null>(null)

interface MeterProps extends PrimitiveMeterProps, Pick<MeterRenderProps, "color"> {}

export function Meter({ className, children, color, ...props }: MeterProps) {
  return (
    <PrimitiveMeter
      data-slot="meter"
      {...props}
      className={cx(
        "w-full",
        "[&>[data-slot=meter-header]+[data-slot=meter-track]]:mt-2",
        "[&>[data-slot=meter-header]+[data-slot=meter-track]]:mt-2",
        "[&>[data-slot=meter-header]+[slot='description']]:mt-1",
        "[&>[slot='description']+[data-slot=meter-track]]:mt-2",
        "[&>[data-slot=meter-track]+[slot=description]]:mt-2",
        "[&>[data-slot=meter-track]+[slot=errorMessage]]:mt-2",
        "*:data-[slot=meter-header]:font-medium",
        className,
      )}
    >
      {(values) => (
        <MeterContext value={{ ...values, color }}>
          {typeof children === "function" ? children(values) : children}
        </MeterContext>
      )}
    </PrimitiveMeter>
  )
}

export function MeterTrack({ className, ...props }: React.ComponentProps<"div">) {
  const { percentage, color } = use(MeterContext)!
  return (
    <div
      data-slot="meter-track"
      className={twMerge(
        "[--meter-height:--spacing(1.5)]",
        "relative h-(--meter-height) w-full overflow-hidden rounded-full bg-secondary outline outline-transparent -outline-offset-1",
        className,
      )}
      {...props}
    >
      <div
        data-slot="meter-fill"
        className="absolute top-0 left-0 h-full rounded-full transition-[width] duration-200 ease-linear will-change-[width] motion-reduce:transition-none forced-colors:bg-[Highlight]"
        style={{ width: `${percentage}%`, backgroundColor: color ?? getMeterColor(percentage) }}
      />
    </div>
  )
}

export function MeterValue({
  className,
  ...props
}: Omit<React.ComponentProps<"span">, "children">) {
  const { valueText } = use(MeterContext)!
  return (
    <span
      data-slot="meter-value"
      className={twMerge("text-base/6 sm:text-sm/6", className)}
      {...props}
    >
      {valueText}
    </span>
  )
}

export function MeterHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="meter-header"
      className={twMerge("flex items-center justify-between", className)}
      {...props}
    />
  )
}

function getMeterColor(value: number): string {
  if (value < 50) return "var(--color-success)"
  if (value < 80) return "var(--color-warning)"
  return "var(--color-danger)"
}

Installation

npx shadcn@latest add @intentui/meter

Usage

import { Meter } from "@/components/ui/meter"
<Meter />