Ir al contenido
Contenido y datos

Meter

Una medida dentro de un rango: disco usado, cupo consumido, ocupación. No es un progreso. Con StackedMeter, la barra de almacenamiento de iCloud.

import { Meter, StackedMeter } from "sebs7n-ui/meter"

Ejemplos

Cuánto del plan se usó

Una medida, no una tarea: el número puede subir o bajar solo —al borrar un archivo el disco se libera— y nunca "termina". Por eso es `role="meter"` y no `role="progressbar"`.

import { Button } from "sebs7n-ui/button"
import { Meter } from "sebs7n-ui/meter"
import { useState } from "react"

function EspacioDelPlan() {
  const [usado, setUsado] = useState(6.4)

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Meter
        format={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
        label="Espacio usado"
        locale="es-AR"
        max={10}
        showValue
        value={usado}
      />
      <div className="flex gap-2">
        <Button onClick={() => setUsado((v) => Math.min(10, v + 1.3))} size="sm" variant="secondary">
          Subir un adjunto
        </Button>
        <Button disabled={usado === 0} onClick={() => setUsado(0)} size="sm" variant="ghost">
          Vaciar la papelera
        </Button>
      </div>
    </div>
  )
}

Con su propio rango y su propio formato

`min` y `max` son el rango real del dato y `format` es el de `Intl.NumberFormat`: lo que se ve y lo que lee el lector salen del mismo texto, así que no se pueden desincronizar. Sin `format`, lo que se anuncia es la proporción ("64%").

import { Meter } from "sebs7n-ui/meter"

function CupoFacturado() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Meter
        format={{ style: "currency", currency: "ARS", maximumFractionDigits: 0 }}
        label="Consumo del mes"
        locale="es-AR"
        max={500_000}
        showValue
        value={321_400}
      />
      <Meter label="Legajos cargados" max={40} showValue value={26} />
      {/* `lg`: la barra de almacenamiento de Settings de iCloud, 16 de alto y radio 6. */}
      <Meter label="Almacenamiento" max={50} showValue size="lg" value={21.5} />
    </div>
  )
}

Tamaños y uso en una lista

`sm` (4px) cuando la barra acompaña una fila y el texto de al lado ya dice el número; `md` (6px) suelta. Sin etiqueta visible va `aria-label`: una barra sin nombre no dice qué está midiendo.

import { Meter } from "sebs7n-ui/meter"

function LicenciasPorEquipo() {
  const equipos = [
    { nombre: "Ventas", licencias: 48, usadas: 41 },
    { nombre: "Soporte", licencias: 30, usadas: 12 },
    { nombre: "Administración", licencias: 22, usadas: 22 },
  ]

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      {equipos.map((equipo) => (
        <div className="flex flex-col gap-1.5" key={equipo.nombre}>
          <div className="flex items-baseline justify-between">
            <span className="text-callout text-label">{equipo.nombre}</span>
            <span className="text-footnote text-label-secondary">
              {equipo.usadas} de {equipo.licencias} licencias
            </span>
          </div>
          <Meter
            aria-label={`Licencias usadas de ${equipo.nombre}`}
            max={equipo.licencias}
            size="sm"
            value={equipo.usadas}
          />
        </div>
      ))}
    </div>
  )
}

Apilado: la barra de almacenamiento

`StackedMeter` apila varias medidas del mismo total, como la barra de Almacenamiento de iCloud: cada segmento es un `meter` con su nombre («Facturas, 13,5 GB»), el resto queda gris, y arriba van el chip del total y «Libre · Usado».

import { StackedMeter } from "sebs7n-ui/meter"

function Apilado() {
  return (
    <StackedMeter
      aria-label="Espacio de la cuenta"
      className="w-full max-w-xl"
      format={{ style: "unit", unit: "gigabyte", maximumFractionDigits: 1 }}
      legend
      locale="es-AR"
      max={50}
      segments={[
        { label: "Facturas", value: 13.5, color: "amber" },
        { label: "Documentos", value: 6.1, color: "purple" },
        { label: "Imágenes", value: 4, color: "teal" },
        { label: "Adjuntos", value: 1.2, color: "blue" },
      ]}
      total="50 GB"
    />
  )
}

Props

Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».

Meter

PropTipoPor defectoDescripción
value*number—Heredada de Base UI. El valor actual, siempre un número: un Meter no tiene estado indeterminado.
labelNonNullable<ReactNode>—El nombre visible de la medida. Es la forma preferida de nombrarla; sin él, el tipo exige aria-label o aria-labelledby.
showValuebooleanfalseMuestra el valor formateado a la derecha.
size"sm" | "md" | "lg""md"sm 4px · md 6px de alto de la pista, los mismos que Progress · lg 16px con radio 6, la barra de almacenamiento de iCloud.
trackClassNamestring—Clases de la pista (el riel gris), por si hay que cambiarle el ancho o el radio.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.
formatIntl.NumberFormatOptions—Heredada de Base UI. Opciones de Intl.NumberFormat. Cambian lo que se ve y lo que se lee, nunca el valor.
localeIntl.LocalesArgument—Heredada de Base UI. El locale de Intl.NumberFormat. Por defecto, el del navegador.

StackedMeter

PropTipoPor defectoDescripción
max*number—El total: el tamaño del plan.
segments*StackedMeterSegment[]—Los segmentos, en orden: { label, value, color } (color de la paleta de Badge). Lo que sobra hasta max queda gris.
formatIntl.NumberFormatOptions—Opciones de Intl.NumberFormat para los valores ({ style: "unit", unit: "gigabyte" }).
labelsPartial<Labels["meter"]>—Los textos de la cabecera («Libre», «Usado»). Le gana al LabelsProvider.
legendbooleanfalseEl desglose debajo de la barra: punto, nombre y valor.
localeIntl.LocalesArgument—El locale de Intl.NumberFormat.
totalReact.ReactNode—El chip del total, a la izquierda de la cabecera: blanco, radio 10, 28/700.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

Teclado

—
No es interactivo.

Accesibilidad

  • Emite role="meter", no role="progressbar": el lector anuncia una medida y no una tarea en curso. Es la diferencia que hace que valga la pena tener los dos componentes.
  • aria-valuetext lleva el valor ya formateado —«62%», «$ 321.400»—, que es más útil que el número crudo de aria-valuenow.
  • Lo que se ve con showValue y lo que se lee salen del mismo texto: no se pueden desincronizar.
  • El nombre es obligatorio y lo exige el tipo: label (visible, la preferida), aria-label o aria-labelledby. Mismo motivo que en Progress.
  • Un valor fuera de rango se recorta contra min y max en vez de desbordar la pista.
  • StackedMeter es un role="group" (con nombre obligatorio) con un `role="meter"` por segmento, nombrado por su label y con el valor formateado en aria-valuetext: el lector dice «Facturas, 13,5 GB» segmento por segmento, no un porcentaje suelto.
  • En StackedMeter el color no es el único dato: cada segmento tiene nombre, y legend (o una List con dot) lo escribe.

Reglas de uso

  • Si el número puede bajar solo, es un `Meter`. Un disco que se libera al borrar un archivo, un cupo que se renueva, una ocupación que sube y baja. Si en cambio arrancó, va para un lado solo y al llegar al final la pantalla cambia de estado, es un Progress.
  • Ese es el error que garantiza que alguien use el equivocado: las dos barras se ven igual, pero un lector de pantalla anuncia cosas distintas, y «subiendo el archivo» no es lo mismo que «6,4 GB de 10».
  • min y max son el rango real del dato: la barra se llena sobre ese rango, no sobre 100.
  • format y locale son los de Intl.NumberFormat. Sin format, lo que se anuncia es la proporción.
  • Poné label o aria-label: una barra sin nombre no dice qué está midiendo.
  • size="sm" cuando acompaña una fila de una lista y el texto de al lado ya dice el número; md suelta.
  • Varias medidas del mismo total → `StackedMeter`, la barra de almacenamiento de iCloud: el chip del total (total), «Libre · Usado» arriba y el desglose con legend o, mejor, una List con ListSection y dot debajo.
  • Hasta cinco o seis segmentos: más que eso no se distinguen en 16 px. Agrupá el resto en «Otros».

Relacionados