# LogViewer

> El visor de logs: monoespaciado, una línea por renglón, con el nivel en color y en texto, que sigue la última línea.

```tsx
import { LogViewer } from "sebs7n-ui/log-viewer"
```

## Ejemplos

### Niveles

`warn` y `error` llevan el nivel escrito además del color; `info` lo dice solo el lector. El visor se recorre con el teclado.

```tsx
import { LogViewer } from "sebs7n-ui/log-viewer"

function Levels() {
  return <LogViewer aria-label="Registro de envíos" className="w-full" lines={LINES} />
}
```

### En vivo

Sigue la última línea mientras el scroll esté al final; si subís a leer, se queda donde estás. Pausar es un botón de la app.

```tsx
import { Button } from "sebs7n-ui/button"
import { LogViewer, type LogLine } from "sebs7n-ui/log-viewer"
import { PauseIcon, PlayIcon } from "lucide-react"
import { useEffect, useState } from "react"

function Live() {
  const [lines, setLines] = useState<LogLine[]>(LINES)
  const [paused, setPaused] = useState(false)
  useEffect(() => {
    if (paused) return
    const timer = setInterval(() => {
      setLines((previous) => [...previous, { id: previous.length + 1, time: new Date().toTimeString().slice(0, 8), message: `Cobro conciliado ${previous.length + 1}` }].slice(-200))
    }, 1000)
    return () => clearInterval(timer)
  }, [paused])
  return (
    <div className="flex w-full flex-col gap-2">
      <div>
        <Button onClick={() => setPaused((value) => !value)} size="sm" variant="secondary">
          {paused ? <PlayIcon /> : <PauseIcon />}
          {paused ? "Reanudar" : "Pausar"}
        </Button>
      </div>
      <LogViewer aria-label="Cobros en vivo" lines={lines} variant="terminal" />
    </div>
  )
}
```

### Cargando y vacío

Cargando dice «Cargando…» y marca la región como ocupada; sin líneas, por qué no hay.

```tsx
import { LogViewer } from "sebs7n-ui/log-viewer"

function States() {
  return (
    <div className="flex w-full flex-col gap-3">
      <LogViewer aria-label="Cargando registro" className="h-24" lines={[]} loading />
      <LogViewer aria-label="Registro vacío" className="h-24" emptyMessage="Ninguna línea coincide con los filtros." lines={[]} />
    </div>
  )
}
```

## Props

### LogViewer

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `lines` * | `readonly LogLine[]` | — | Las líneas, de la más vieja a la más nueva: `{ id?, time?, level?, source?, message }`. |
| `aria-label` | `string` | — | Qué se está mirando: «Log del despliegue». Por defecto, `labels.label`. |
| `emptyMessage` | `React.ReactNode` | — | Lo que se ve sin líneas (por ejemplo, «Ninguna línea coincide con los filtros»). Por defecto, `labels.empty`. |
| `follow` | `boolean` | `true` | Sigue la última línea mientras el scroll esté al final; si se sube a leer, se queda ahí. Por defecto, `true`. |
| `labels` | `Partial<LogViewerLabels>` | — | Textos: `label`, `empty`, `loading` y los niveles `info`, `warn` y `error`. Por defecto, `logViewerLabels`. |
| `loading` | `boolean` | `false` | Trayendo las líneas: muestra «Cargando…» y marca la región como ocupada. |
| `onScroll` | `UIEventHandler<T>` | — | Corre en cada scroll del visor, después de que el componente anota si quedó al final. |
| `variant` | `"default" \| "terminal"` | `"default"` | `terminal`: fondo oscuro en los dos temas, como una terminal. Por defecto, `default`: el relleno gris de la superficie. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Entra al visor, que es enfocable para recorrerlo. |
| ↑ ↓ · Re Pág · Av Pág · Inicio · Fin | Se desplazan por las líneas (el scroll nativo del navegador). |

## Accesibilidad

- Es una región `role="log"` con nombre (`aria-label`: «Log del despliegue»): el lector anuncia las líneas nuevas sin robar el foco. `tabIndex={0}` permite recorrerlo con el teclado.
- El nivel va en color **y** en texto: `warn` y `error` llevan «Aviso:» / «Error:» a la vista, e `info` lo dice solo para el lector (`sr-only`). Nunca solo color.
- `aria-busy` mientras carga; sin líneas dice por qué (`emptyMessage`).

## Reglas de uso

- `lines`: `{ id?, time?, level?, source?, message }`, de la más vieja a la más nueva, ya filtradas y acotadas por la app (los últimos 500, por ejemplo). Pausar, buscar y descargar son botones de la app, en una `FilterBar`.
- `follow` (por defecto) baja solo cuando llegan líneas, pero **solo si el scroll estaba al final**: quien subió a leer no pierde el lugar.
- `variant="terminal"` lo pone oscuro en los dos temas, como una terminal. La altura se fija con `className` (`h-[60dvh]`).
- Solo por subpath (`sebs7n-ui/log-viewer`).

## Relacionados

[filter-bar](/docs/components/filter-bar.md) · [scroll-area](/docs/components/scroll-area.md) · [empty-state](/docs/components/empty-state.md)
