# WidgetBoard

> Un panel de widgets que el usuario edita, como la pantalla de inicio de iCloud: una grilla estática de cards que, con «Editar», se reordena arrastrando o con el teclado, saca widgets con su «−», los vuelve a agregar desde un catálogo y se restablece. Se guarda en `localStorage`.

```tsx
import { WidgetBoard, WidgetBoardEditButton } from "sebs7n-ui/widget-board"
```

## Ejemplos

### Panel editable

«Editar» pone los widgets en modo edición: tiemblan, se reordenan arrastrando o con Espacio y las flechas, se sacan con su «−» y vuelven desde «Agregar widget». «Restablecer» deja el panel original. El orden se guarda en este navegador.

```tsx
import { WidgetBoard, WidgetBoardEditButton } from "sebs7n-ui/widget-board"
import { useWidgetLayout } from "sebs7n-ui/lib/widget-layout"

function Editable() {
  const layout = useWidgetLayout({ storageKey: "demo:widget-board", widgets: WIDGETS })
  return (
    <div className="@container flex w-full flex-col gap-4">
      <div className="flex justify-end">
        <WidgetBoardEditButton layout={layout} size="sm" />
      </div>
      <WidgetBoard layout={layout} />
    </div>
  )
}
```

## Props

### WidgetBoard

Hereda las props de `<div>`.

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `layout` * | `WidgetLayout` | — | El estado: lo que devuelve `useWidgetLayout`. |
| `gridClassName` | `string` | — | Clases de la grilla, por ejemplo otro `gap`. |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

### WidgetBoardEditButton

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `layout` * | `WidgetLayout` | — | El estado: lo que devuelve `useWidgetLayout`. |
| `loading` | `boolean` | — | Como en `Button`: deshabilita y marca el botón como ocupado. |
| `size` | `"sm" \| "md" \| "lg" \| "icon-sm" \| "icon-md" \| "icon-lg"` | — | El tamaño del botón, como en `Button`. En la cabecera de una pantalla, `md` (por defecto). |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Recorre los botones de la barra de edición, los «−» y, en cada card, lo interactivo de adentro. |
| Espacio | En un widget en edición lo toma; con otro Espacio lo suelta en su nuevo lugar. |
| ← → ↑ ↓ | Con un widget tomado, lo mueven por la grilla. |
| Escape | Cancela el movimiento; sin un widget tomado, sale de la edición. |
| Enter · Espacio | En un «−» saca el widget; en una fila del catálogo lo agrega. |

## Accesibilidad

- La grilla es una lista con nombre («Widgets»). Entrar en edición, sacar, agregar, mover y restablecer se anuncian en español en regiones `role="status"`.
- Cada «−» y cada fila del catálogo se llaman con el widget («Sacar Clientes», «Agregar Clientes»). Al sacar uno el foco pasa al «−» que queda (o a «Agregar widget» si no queda ninguno); al agregar, al «−» del nuevo.
- Con movimiento reducido el temblor se apaga y un contorno punteado dice que está en edición. La vista previa del catálogo es decorativa (`aria-hidden`).
- Sin widgets en pantalla hay un `EmptyState` con «Agregar widget» y «Restablecer»: el panel nunca queda mudo.

## Reglas de uso

- Tres piezas: `useWidgetLayout({ storageKey, widgets })` (el estado), `<WidgetBoard layout />` (la grilla) y `<WidgetBoardEditButton layout />` (el «Editar»/«Listo» de la cabecera, secundario: la acción primaria sigue siendo la de la pantalla).
- Cada widget es `{ id, title, size?, description?, preview?, icon?, render }`. `size`: `sm` (1 columna de 4), `md` (2) o `lg` (4); por debajo de 36 rem de ancho del contenedor son 2 columnas y por debajo de 56 rem, 1. Las cards de una fila quedan alineadas: la card llena el alto de su celda.
- **Carga diferida:** sin editar es una grilla estática (sin `@dnd-kit`); el arrastre, la barra y el catálogo se piden la primera vez que se aprieta «Editar» (`React.lazy`), con la misma grilla de fondo mientras llegan. Los peers opcionales `@dnd-kit/*` hacen falta solo si se usa.
- El HTML del servidor muestra el orden original; lo guardado se adopta después de montar. Si el panel depende de un proyecto o una cuenta, una clave por cada uno y `key` en la pantalla para que el estado no se arrastre.
- Solo por subpath (`sebs7n-ui/widget-board`); el modelo puro (`moveWidget`, `removeWidget`, `addWidget`, `reorderWidgets`, `serializeLayout`) sale de `sebs7n-ui/lib/widget-layout`.

## Relacionados

[sortable-grid](/docs/components/sortable-grid.md) · [widget-card](/docs/components/widget-card.md) · [stat-grid](/docs/components/stat-grid.md) · [empty-state](/docs/components/empty-state.md) · [popover](/docs/components/popover.md)
