UNPKG

@codeworker.br/govbr-tw-react

Version:

Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.

93 lines (92 loc) 4.46 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useMemo, useState } from "react"; import { cva } from "class-variance-authority"; /** * ProgressBar – barra de progresso com temas/variantes, labels opcionais e cores custom. * - ÚNICA fonte de verdade para variantes via cva (sem switch duplicado) * - trackClassName / progressClassName continuam podendo sobrescrever estilos * - Labels respeitam o tema e aceitam variante separada * - NOVO: label some em 100% e anima "..." quando o texto terminar com "..." */ const trackVariants = cva("w-full overflow-hidden rounded-md", { variants: { theme: { light: "bg-govbr-gray-10", dark: "bg-govbr-blue-warm-20/30 supports-[color-scheme:dark]:bg-govbr-blue-warm-20/40", }, density: { lowest: "h-7", low: "h-5", default: "h-4", high: "h-3", }, }, defaultVariants: { theme: "light", density: "default", }, }); const barVariants = cva("h-full transition-[width] duration-300 ease-out", { variants: { theme: { light: "", dark: "" }, variant: { default: "bg-govbr-blue-warm-vivid-70", success: "bg-govbr-green-cool-vivid-50", danger: "bg-govbr-red-vivid-50", warning: "bg-govbr-yellow-vivid-20", info: "bg-govbr-gray-60", }, }, defaultVariants: { theme: "light", variant: "default" }, }); const labelWrapVariants = cva("mb-1 flex items-center justify-between text-sm", { variants: { theme: { light: "text-govbr-gray-80", dark: "text-govbr-pure-0", }, }, defaultVariants: { theme: "light" }, }); const percentVariants = cva("font-medium tabular-nums", { variants: { theme: { light: "", dark: "" }, variant: { default: "text-inherit", success: "text-inherit", danger: "text-inherit", warning: "text-inherit", info: "text-inherit", }, }, defaultVariants: { theme: "light", variant: "default" }, }); function cx(...classes) { return classes.filter(Boolean).join(" "); } export default function ProgressBar({ value, showLabels = true, label = "Carregando ...", trackClassName, progressClassName, className, theme, variant, density, labelVariant, labelClassName, hideLabelOnComplete = true, progresslabel = "" }) { const pct = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0)); // Detecta se o label termina com "..." const endsWithEllipsis = useMemo(() => /\.\.\.$/.test(label), [label]); const baseLabel = useMemo(() => (endsWithEllipsis ? label.slice(0, -3) : label), [label, endsWithEllipsis]); // Animação dos três pontos: 0 → 1 → 2 → 3 → 0 (0 = sem pontos) const [dotCount, setDotCount] = useState(0); const shouldAnimate = progresslabel === "" ? showLabels && pct < 100 && endsWithEllipsis : false; useEffect(() => { if (!shouldAnimate) { // Quando não deve animar, mantém o label estável setDotCount(endsWithEllipsis ? 3 : 0); return; } const id = setInterval(() => { setDotCount((d) => (d + 1) % 4); }, 500); // ajuste fino da velocidade aqui return () => clearInterval(id); }, [shouldAnimate, endsWithEllipsis]); const displayLabel = endsWithEllipsis ? `${baseLabel}${".".repeat(dotCount)}` : label; const trackCls = cx(trackVariants({ theme, density }), trackClassName); const barCls = cx(barVariants({ theme, variant }), progressClassName); const labelsWrap = labelWrapVariants({ theme }); const percentCls = percentVariants({ theme, variant: labelVariant || variant }); return (_jsxs("div", { className: cx("w-full", className), children: [showLabels && (_jsxs("div", { className: cx(labelsWrap, labelClassName), children: [_jsx("span", { "aria-live": "polite", children: pct === 100 && hideLabelOnComplete ? "" : displayLabel }), _jsx("span", { className: percentCls, children: pct === 100 && hideLabelOnComplete ? "" : `${progresslabel === "" ? `${pct}%` : progresslabel}` })] })), _jsx("div", { className: trackCls, role: "progressbar", "aria-valuenow": pct, "aria-valuemin": 0, "aria-valuemax": 100, "aria-label": label, children: _jsx("div", { className: barCls, style: { width: `${pct}%` } }) })] })); }