Ir al contenido
Evaluar mi proyecto
Componentes · Datos y visualizaciónDisponible

Indicador analógico de tablero

Arco con zonas verde, ámbar y rojo, aguja con resorte y pantalla LCD; skeuomórfico oscuro con valor ajustable.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

datos-gauge.tsx95 líneas
Descargar
"use client";

import { useState, type CSSProperties } from "react";

/** Indicador analógico de tablero skeuomórfico: arco con zonas de color, aguja que oscila con resorte y pantalla digital. Valor controlable con teclado. React + TypeScript, sin dependencias. */
export type DatosGaugeProps = {
  value?: number;
  defaultValue?: number;
  min?: number;
  max?: number;
  label?: string;
  unit?: string;
  onChange?: (value: number) => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcdga-root { box-sizing: border-box; display: inline-grid; justify-items: center; gap: 8px; width: 280px; max-width: 100%; padding: 22px 18px 18px; border-radius: 24px; background: linear-gradient(180deg, #2c2e34, #1b1c20); color: #dfe2ea; font: 600 12px/1 "Helvetica Neue", Arial, ui-sans-serif, sans-serif; letter-spacing: .14em; text-transform: uppercase; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .14), inset 0 -2px 6px rgba(0, 0, 0, .5), 0 18px 28px -12px rgba(0, 0, 0, .7); }
.gcdga-root *, .gcdga-root *::before, .gcdga-root *::after { box-sizing: border-box; }
.gcdga-root button { font: inherit; letter-spacing: inherit; text-transform: inherit; }
.gcdga-root :focus-visible { outline: 2px solid #5aa9ff; outline-offset: 3px; border-radius: 8px; }
@media (prefers-reduced-motion: reduce) { .gcdga-root * { transition-duration: .01ms !important; } }
.gcdga-dial { position: relative; width: 240px; height: 140px; max-width: 100%; }
.gcdga-dial svg { width: 100%; height: 100%; overflow: visible; }
.gcdga-needle { transform-origin: 120px 120px; transition: transform .9s cubic-bezier(.34, 1.4, .5, 1); }
.gcdga-lcd { min-width: 110px; padding: 8px 12px; border-radius: 6px; background: #0b140d; color: #7dffa1; text-align: center; font: 600 22px/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: .08em; text-shadow: 0 0 8px rgba(125, 255, 161, .6); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .9), 0 1px 0 rgba(255, 255, 255, .15); font-variant-numeric: tabular-nums; }
.gcdga-adj { display: flex; gap: 8px; }
.gcdga-adj button { width: 40px; height: 32px; border: 0; border-radius: 6px; background: linear-gradient(180deg, #4a4d55, #34363c); color: #fff; font-size: 18px; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 2px 0 #121316; }
.gcdga-adj button:active { transform: translateY(1px); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25); }`;

const polar = (cx: number, cy: number, r: number, deg: number) => {
  const a = ((deg - 180) * Math.PI) / 180;
  return [cx + r * Math.cos(a), cy + r * Math.sin(a)] as const;
};
function arc(r: number, from: number, to: number) {
  const [x1, y1] = polar(120, 120, r, from);
  const [x2, y2] = polar(120, 120, r, to);
  return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${to - from > 180 ? 1 : 0} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`;
}

export function DatosGauge({ value, defaultValue = 68, min = 0, max = 100, label = "Carga del sistema", unit = "%", onChange, className = "", style }: DatosGaugeProps) {
  const [inner, setInner] = useState(defaultValue);
  const v = Math.max(min, Math.min(max, value ?? inner));
  const t = (v - min) / (max - min);
  const set = (next: number) => {
    const clamped = Math.max(min, Math.min(max, next));
    setInner(clamped);
    onChange?.(clamped);
  };
  const zones = [
    { from: 0, to: 0.6, c: "#38c172" },
    { from: 0.6, to: 0.85, c: "#f6c343" },
    { from: 0.85, to: 1, c: "#ef5350" },
  ];
  return (
    <div className={`gcdga-root ${className}`} style={style} role="meter" aria-label={label} aria-valuemin={min} aria-valuemax={max} aria-valuenow={Math.round(v)} aria-valuetext={`${Math.round(v)}${unit}`}>
      <style>{CSS}</style>
      <div className="gcdga-dial">
        <svg viewBox="0 0 240 140" aria-hidden="true">
          <path d={arc(100, 0, 180)} fill="none" stroke="#0e0f12" strokeWidth="20" strokeLinecap="round" />
          {zones.map((zone) => (
            <path key={zone.c} d={arc(100, zone.from * 180 + (zone.from === 0 ? 0 : 1.5), zone.to * 180 - (zone.to === 1 ? 0 : 1.5))} fill="none" stroke={zone.c} strokeWidth="14" strokeLinecap="butt" opacity=".92" />
          ))}
          {Array.from({ length: 21 }, (_, i) => {
            const deg = (i / 20) * 180;
            const [x1, y1] = polar(120, 120, 82, deg);
            const [x2, y2] = polar(120, 120, i % 5 === 0 ? 70 : 76, deg);
            return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke="#8d92a0" strokeWidth={i % 5 === 0 ? 2 : 1} />;
          })}
          <g className="gcdga-needle" style={{ transform: `rotate(${t * 180 - 90}deg)` }}>
            <path d="M120 120 L116 118 L120 36 L124 118 Z" fill="#ff5b4a" />
            <path d="M120 120 L120 36" stroke="rgba(255,255,255,.4)" strokeWidth="1" />
          </g>
          <circle cx="120" cy="120" r="12" fill="#cfd2d9" stroke="#0e0f12" strokeWidth="3" />
          <circle cx="120" cy="120" r="4" fill="#4a4d55" />
        </svg>
      </div>
      <span className="gcdga-lcd" aria-hidden="true">
        {Math.round(v)}
        {unit}
      </span>
      <span aria-hidden="true">{label}</span>
      <div className="gcdga-adj">
        <button type="button" aria-label="Reducir" onClick={() => set(v - 10)}>
          −
        </button>
        <button type="button" aria-label="Aumentar" onClick={() => set(v + 10)}>
          +
        </button>
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/datos-gauge

Ejemplo de uso de Indicador analógico de tablero

Después de copiar datos-gauge.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { DatosGauge } from "./datos-gauge";

export default function Ejemplo() {
  return <DatosGauge label="Ejemplo" />;
}

Propiedades de DatosGauge

DatosGauge acepta 9 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
value?numberOpcional.
defaultValue?numberOpcional.
min?numberOpcional.
max?numberOpcional.
label?stringOpcional.
unit?stringOpcional.
onChange?(value: number) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 95 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Indicador analógico de tablero por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: meter.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)