import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cn } from "../../utils.js"; import "./meter.css"; export type MeterVariant = "default" | "brand" | "success" | "warning" | "danger"; export type MeterSize = "sm" | "md" | "lg"; export interface MeterProps extends Omit, "children"> { value: number; min?: number; max?: number; low?: number; high?: number; optimum?: number; variant?: MeterVariant; size?: MeterSize; label?: React.ReactNode; formatValue?: (value: number, min: number, max: number) => React.ReactNode; showValue?: boolean; asChild?: boolean; children?: React.ReactNode; } interface MeterContextValue { value: number; min: number; max: number; percentage: number; variant: MeterVariant; size: MeterSize; low?: number; high?: number; optimum?: number; } const MeterContext = React.createContext(null); export function useMeterContext() { const context = React.useContext(MeterContext); if (!context) { throw new Error("Meter compound components must be used within a Meter"); } return context; } function resolveVariant( explicitVariant: MeterVariant | undefined, value: number, low?: number, high?: number, optimum?: number ): MeterVariant { if (explicitVariant) return explicitVariant; if (low !== undefined && high !== undefined) { if (optimum !== undefined) { if (optimum >= high) { if (value >= high) return "success"; if (value >= low) return "warning"; return "danger"; } else if (optimum <= low) { if (value <= low) return "success"; if (value <= high) return "warning"; return "danger"; } } if (value > high) return "danger"; if (value > low) return "warning"; return "success"; } return "brand"; } export const Meter = React.forwardRef( ( { className, value, min = 0, max = 100, low, high, optimum, variant: explicitVariant, size = "md", label, formatValue = (val) => `${Math.round(val)}%`, showValue = true, asChild = false, children, ...props }, ref ) => { const clampedValue = Math.min(Math.max(value, min), max); const range = max - min; const percentage = range > 0 ? ((clampedValue - min) / range) * 100 : 0; const variant = resolveVariant(explicitVariant, clampedValue, low, high, optimum); const contextValue = React.useMemo( () => ({ value: clampedValue, min, max, percentage, variant, size, low, high, optimum, }), [clampedValue, min, max, percentage, variant, size, low, high, optimum] ); const Comp = asChild ? Slot : "div"; return ( {children ? ( children ) : ( <> {(label || showValue) && (
{label && {label}} {showValue && ( {formatValue(clampedValue, min, max)} )}
)} {/* Accessible hidden native meter for assistive tech */} )}
); } ); Meter.displayName = "Meter"; export interface MeterLabelProps extends React.HTMLAttributes { asChild?: boolean; } export const MeterLabel = React.forwardRef( ({ className, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "span"; return ( ); } ); MeterLabel.displayName = "MeterLabel"; export interface MeterValueProps extends React.HTMLAttributes { asChild?: boolean; } export const MeterValue = React.forwardRef( ({ className, asChild = false, children, ...props }, ref) => { const { value, min, max } = useMeterContext(); const Comp = asChild ? Slot : "span"; return ( {children ?? `${Math.round(((value - min) / (max - min || 1)) * 100)}%`} ); } ); MeterValue.displayName = "MeterValue"; export interface MeterTrackProps extends React.HTMLAttributes { asChild?: boolean; } export const MeterTrack = React.forwardRef( ({ className, asChild = false, children, ...props }, ref) => { const { size } = useMeterContext(); const Comp = asChild ? Slot : "div"; return ( {children ?? } ); } ); MeterTrack.displayName = "MeterTrack"; export interface MeterIndicatorProps extends React.HTMLAttributes { asChild?: boolean; } export const MeterIndicator = React.forwardRef< HTMLDivElement, MeterIndicatorProps >(({ className, asChild = false, style, ...props }, ref) => { const { percentage, variant } = useMeterContext(); const Comp = asChild ? Slot : "div"; return ( ); }); MeterIndicator.displayName = "MeterIndicator";