# Sistema de diseño — Guía de uso y extensión

> Documento normativo. La capa de estilos vive en `public/assets/css/app.css`
> (26 secciones, tokens + componentes) y el comportamiento compartido en
> `public/assets/js/app.js` (objeto global `PV`).
>
> **Regla de oro:** los nombres de clase son estables. Un módulo nuevo se
> construye **combinando las clases existentes**; no se inventan variantes ad
> hoc ni se redefinen estilos desde un módulo. Si falta un componente, se
> añade **en `app.css`** (sección correspondiente) y se documenta aquí.

---

## 1. Principios

1. **La información va primero.** En un ERP la tabla manda: el 70 % del
   espacio de una pantalla es información operativa. Se evita el tablero de
   tarjetas gigantes y el «muro de gráficas».
2. **Un veredicto por pantalla.** Cada vista debe responder de inmediato
   «¿cómo vamos?». El panel muestra el estado; cada módulo, sus KPIs.
3. **Densidad es una preferencia del usuario**, no un capricho del diseñador.
   El sistema ofrece dos densidades de tabla (cómoda y compacta).
4. **Consistencia por encima de laakedirs.** El mismo tipo de acción se ve
   igual en todas las pantallas: mismo icono, mismo peso tipográfico, mismo
   color.
5. **Sin dependencias externas.** Ni fuentes web ni librerías de iconos. La
   tipografía es la pila del sistema y los iconos son SVG en línea
   (`App\Support\Icon`).
6. **La lógica de negocio no se toca por motivos visuales.** Un cambio
   visual que altere consultas, endpoints o reglas presupuestales está fuera
   de alcance.

---

## 2. Tokens

Los tokens son variables CSS declaradas en `:root`. **Nunca se escribe un
color, un radio o una sombra en hexadecimal dentro de una vista o de un módulo.**

### 2.1 Identidad institucional

```
--brand-primary     #1f3a5f    --brand-50  …  --brand-900  (rampa de 10 pasos)
--brand-secondary   #2e5a88
--brand-accent      #c9a227    (oro institucional)
--brand-soft / --brand-line / --accent-soft / --accent-line
```

El layout (`app/Views/layouts/app.php`) **sobrescribe** `--brand-primary`,
`--brand-secondary` y `--brand-accent` en runtime con los colores que el
usuario configuró en *Configuración → Identidad*. Por eso un módulo **nunca**
debe fijar el color de marca por su cuenta: usa `var(--brand-primary)`.

### 2.2 Neutros y superficies

```
--gray-0 … --gray-900      gris frío con matiz azulado (no gris neutro)
--bg-canvas / --bg-surface / --bg-elevated / --bg-subtle / --bg-muted / --bg-inset
--bg-sidebar / --bg-sidebar-hover / --bg-sidebar-active
```

### 2.3 Texto

```
--text-strong     títulos y cifras
--text-body       cuerpo de texto
--text-muted      metadatos, ayudas
--text-subtle     texto terciario (no usar para información obligatoria)
--text-inverse    sobre fondo de color
```

### 2.4 Bordes y estados

```
--border-color / --border-strong / --border-subtle / --border-focus
--color-success / --color-warning / --color-danger / --color-info
  …y sus pares --color-*-bg y --color-*-border
```

### 2.5 Tipografía

| Token | Valor | Uso |
|---|---|---|
| `--font-size-xs` | 12 px | Etiquetas de columna, metadatos mínimos |
| `--font-size-sm` | 13 px | Metadatos, pie de tabla |
| `--font-size-base` | 15 px | **Cuerpo de texto** |
| `--font-size-md` | 16 px | Controles de formulario, títulos de tarjeta |
| `--font-size-lg` | 18 px | Subtítulos |
| `--font-size-xl` | 22 px | Títulos de sección |
| `--font-size-2xl` | 28 px | Títulos de página, cifras KPI |
| `--font-size-3xl` | 34 px | Cifras destacados del panel |

Pilas: `--font-sans` (sistema) y `--font-mono` (cifras técnicas, folios).
Interlineados: `--line-height-tight` (1.25) / `--line-height` (1.5) /
`--line-height-relaxed` (1.65). Tracking: `--tracking-tight` para títulos
grandes, `--tracking-label` para etiquetas en versalitas.

> **Cifras tabulares:** toda cifra que se pueda comparar en vertical lleva la
> clase `tabular` (`font-variant-numeric: tabular-nums`). Sin ella las
> columnas de importes bailan al paginar.

### 2.6 Espaciado, radios, sombras, movimiento

- Espaciado: `--space-1` … `--space-16` (base 4 px).
- Radios: `--radius-xs` 5 px · `--radius-sm` 8 px · `--radius-md` 11 px ·
  `--radius-lg` 15 px · `--radius-xl` 20 px · `--radius-full`.
- Sombras: `--shadow-xs` → `--shadow-xl`, siempre **en capas** (nunca una
  sombra plana) y `--shadow-inset` para el brillo superior sutil.
- Movimiento: `--transition` 160 ms (estados) y `--transition-slow` 260 ms
  (apariciones). Curvas `--ease` y `--ease-out`.

### 2.7 Tema

El tema se declara con `data-theme="light|dark"` en `<html>` y se persiste en
la cookie `pv_theme`.

```html
<html lang="es" data-theme="light" data-density="comfortable">
```

**El tema de la aplicación manda sobre el del sistema operativo.** Por eso
`:root` declara `color-scheme: light` y `[data-theme="dark"]` declara
`color-scheme: dark`. Si se olvida, los controles nativos (checkbox, radio,
`select`, calendario, barras de desplazamiento) se dibujan con la paleta del
sistema operativo y aparecen oscuros dentro de una interfaz clara.

---

## 3. Estructura de una pantalla

```
<main class="app-main">
  <header class="page-header">
    <div>
      <h1 class="page-header__title">…</h1>
      <p class="page-header__subtitle">…</p>
    </div>
    <div class="page-header__actions">…</div>
  </header>
  <nav class="breadcrumb">…</nav>
  <section class="card">…</section>
</main>
```

- `page-header` → título + acción principal. **Una sola acción `btn--primary`
  por pantalla.**
- `breadcrumb` → miga de pan.
- `card` → contenedor de contenido. Cabecera (`card__header` + `card__title` +
  `card__subtitle`), cuerpo (`card__body`), pie (`card__footer`).

---

## 4. Componentes

### 4.1 Botones

| Clase | Uso |
|---|---|
| `btn btn--primary` | Acción principal de la pantalla |
| `btn btn--secondary` | Acción alternativa o de contexto |
| `btn btn--ghost` | Acción terciaria, dentro de un conjunto |
| `btn btn--success` / `btn--danger` | Confirmación positiva / destructiva |
| `btn btn--accent` | Acción de énfasis institucional (poco uso) |
| `btn btn--sm` | Tamaño reducido (toolbar, tablas) |
| `btn btn--lg` | Acción principal en login e instalador |
| `btn btn--icon` / `icon-btn` | Solo icono; **siempre** con `aria-label` y `title` |
| `btn btn--block` | Ancho completo (login, errores) |
| `btn-group` | Agrupa acciones relacionadas |

Convención de icono (**obligatoria** en botones de acción):

```php
<button class="btn btn--primary" type="button"><?= Icon::render('file-check') ?> Emitir orden</button>
```

Icono **sí**: guardar, emitir, descargar, imprimir, vista previa, duplicar,
agregar, filtrar, limpiar, volver, exportar, cancelar/destruir.
Icono **no**: los botones de descarte de un modal (*Cancelar*, *Cerrar*), que
son deliberadamente neutros, y los conmutadores segmentados de las gráficas.

### 4.2 Formularios

```html
<div class="form-field">
  <label class="form-label" for="x">Monto <span class="form-label__required">*</span></label>
  <input class="input" id="x" type="text">
  <p class="form-hint">Texto de ayuda o error.</p>
</div>
```

- `.form-field` agrupa etiqueta + control + ayuda. Es obligatorio.
- `.form-label` + `.form-label__required` (`*`).
- `.input`, `.select`, `.textarea`; `.input--money` para importes (alineación
  a la derecha + cifras tabulares).
- `.form-hint` para la ayuda; los errores se inyectan con `PV.showErrors()`
  y la clase `is-invalid`.
- Rejillas: `.form-grid`, `.form-grid--2`, `.form-grid--3`.
- Casillas: `<label class="checkbox"><input type="checkbox"> <span class="checkbox__text">…</span></label>`.
- Interruptor: `<label class="switch"><input type="checkbox"><span class="switch__track"></span></label>`.

**Importes:** el campo es texto/número; el formato y el cálculo los pone el
backend (`App\Support\Money`). El navegador es informativo.

### 4.3 Tablas

```html
<div class="table-wrapper">
  <table class="table">
    <thead><tr><th class="is-sortable">…</th></tr></thead>
    <tbody>…</tbody>
  </table>
</div>
```

- `.table-wrapper` da el scroll horizontal **dentro** del contenedor, nunca en
  la página. `.table` es la clase base; `.table--compact` reduce el padding.
- Columnas ordenables: `th.is-sortable` (+ `th.is-sorted` y `span.sort-icon`
  los añade `PV.DataTable`).
- Importes: `class="table__num"` (alineación a la derecha + cifras
  tabulares) o `text-right tabular`.
- Etiquetas: `.table__primary` (la principal de la fila) y
  `.table__secondary` (la secundaria). Acciones de fila: `.table__actions`.
- `tfoot` para las filas de totales.
- La densidad se hereda de `<html data-density>`; no se fija por tabla.
- Los estados (borrador, emitida, cancelada) van en `.badge`, nunca en texto
  suelto.

### 4.4 Indicadores (KPI)

```html
<div class="stat-card stat-card--success">
  <div class="stat-card__top">
    <div>
      <div class="stat-card__label">Importe comprometido</div>
      <div class="stat-card__value stat-card__value--sm">$1,234,567.00</div>
      <div class="stat-card__meta">…</div>
    </div>
    <div class="stat-card__icon"><?= Icon::render('wallet') ?></div>
  </div>
</div>
```

La etiqueta tiene altura fija de dos renglones: así **las cifras quedan
alineadas** entre tarjetas de distintos tamaños de texto. El tono semántico
va en la clase del contenedor: `stat-card--success|--warning|--danger|--info|
--accent`.

### 4.5 Insignias, alertas, notificaciones

- `.badge` + `.badge--success|--warning|--danger|--info|--neutral|--accent`,
  con `.badge__dot` opcional.
- `.alert` + `.alert--success|--warning|--danger|--info`, con `.alert__body`
  y `.alert__title`.
- Avisos efímeros: `PV.toast(mensaje, 'success'|'error'|'warning'|'info', título)`.

### 4.6 Modales

```html
<div class="modal" id="miModal" role="dialog" aria-modal="true" aria-labelledby="miModalTitulo">
  <div class="modal__header">…</div>
  <div class="modal__body">…</div>
  <div class="modal__footer">…</div>
</div>
```

- Abrir: `PV.openModal('miModal')`; cerrar: `PV.closeModal('miModal')` o el
  atributo `data-modal-close="miModal"`.
- `PV.confirm({…})` para confirmaciones; el foco queda atrapado dentro del
  modal mientras está abierto.
- Botón de cierre: `btn--icon` **con** `aria-label`.

### 4.7 Estados vacíos, carga y esqueleto

- `.empty-state` con icono, título, texto y acción. Un estado vacío **nunca**
  es una celda de texto suelta.
- `PV.setBusy(boton, true, 'Guardando…')` deshabilita el botón y muestra el
  texto de espera.
- `PV.DataTable` pinta un esqueleto (`.skeleton`) mientras carga.

### 4.8 Toolbar, filtros y paginación

```
.toolbar
  .toolbar__search    → .input (búsqueda principal)
  .toolbar__filters   → .input / .select (filtros)
  .toolbar__spacer    → empuja las acciones a la derecha
.pagination  → .pagination__info + .pagination__pages
```

### 4.9 Paleta de órdenes (captura)

`.order-items` (tabla de conceptos editable), `.order-totals` y
`.order-totals__row` (subtotal / IVA / total, con
`.order-totals__row--total` para la línea de total). Dan el aspecto de un
formulario oficial sin inventar un patrón nuevo.

### 4.10 Paleta de comandos

`Ctrl`/`⌘` + `K` abre la paleta. **No es una función nueva**: lee la barra
lateral real (respeta los permisos del usuario) y consulta APIs de solo
lectura ya existentes. No se le añaden entradas a mano.

### 4.11 Pantalla de acceso
`.auth-page` (degradado de fondo con grano), `.auth-shell`, `.auth-card` (tarjeta oscura
flotante), `.auth-card__media` / `.auth-card__frame` (lado izquierdo), `.auth-card__photo`
(imagen elegible), `.auth-card__caption` (escudo + nombre, al pie), `.auth-card__panel`
(formulario) y `.auth-card__submit` (botón de acceso).

- La tarjeta es **siempre oscura**, con independencia del tema: funciona como una
  puerta de entrada de marca.
- El degradado de fondo se deriva de `--brand-primary`, así que cambia con la identidad.
- El botón usa `--brand-accent` mezclado con blanco: garantiza contraste con texto
  oscuro sea cual sea el acento elegido.
- La imagen de la izquierda viene de `identity.login_image`; sin ella, el panel muestra
  la marca institucional.

### 4.12 Vista previa de la apariencia

`.appearance-preview` con `.appearance-preview__sidebar` (barra con el color
elegido), `.appearance-preview__link` y `.appearance-preview__body` (muestra la
tipografía y el tamaño elegidos). Es una simulación: al guardar, el cambio se
aplica a toda la aplicación.

### 4.13 Asistente de inteligencia artificial

`.ai-layout` (columna principal + lateral de historial), `.ai-chip`
(sugerencias), `.ai-tools` / `.ai-tool` (catálogo de herramientas) y
`.ai-report` con sus partes (`__header`, `__question`, `__summary`,
`__section`, `__list`, `__table-note`, `__footer`, `__sources`).

- El informe se renderiza en el servidor (`app/Views/partial/ai-report.php`)
  y se inyecta en la página; el JavaScript no construye HTML.
- La narrativa del modelo **se escapa siempre**: es contenido externo.
- La tabla la compone el backend con los datos consultados, no el modelo.

### 4.14 Carga de archivos y progreso

**Zona de arrastre** (`.dropzone`): sustituye al selector nativo de archivos.

```html
<input class="sr-only" type="file" id="archivo" accept=".csv,.txt,.xlsx">
<div class="dropzone" id="zonaArchivo" role="button" tabindex="0">
  <div class="dropzone__icon">…</div>
  <p class="dropzone__title">Arrastre el archivo aquí o haga clic para seleccionarlo</p>
  <p class="dropzone__hint">CSV, TXT o XLSX · hasta 5 MB</p>
</div>
<div class="dropzone__file hidden">
  <span class="dropzone__file-name"></span>
  <span class="dropzone__file-meta"></span>
  <button type="button" class="btn btn--sm btn--ghost">Quitar</button>
</div>
```

- El `<input type="file">` real queda oculto con `.sr-only`: la zona actúa
  como su botón (`role="button"`, `tabindex="0"`, Enter y Espacio) y el
  JavaScript lo dispara. El archivo **arrastrado** se guarda en memoria; no
  depende de `input.files`.
- Estados: `.is-dragging` (el archivo está encima) y `.has-file` (ya hay uno).
- La tarjeta `.dropzone__file` va **fuera** de la zona: un botón dentro de un
  `role="button"` anidado rompe la navegación por teclado.

**Área de progreso** (`.progress-area`):

```html
<div class="progress-area" role="status" aria-live="polite">
  <div class="progress-area__head">
    <span class="progress-area__label">Enviando archivo…</span>
    <span class="progress-area__value">27 %</span>
  </div>
  <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="27">
    <div class="progress__bar" style="width:27%"></div>
  </div>
  <p class="progress-area__detail">Se envía «datos.csv» al servidor.</p>
</div>
```

- `.progress` / `.progress__bar` son la barra; los tonos van en
  `.progress__bar--success`, `--warning` y `--danger`.
- Con `.progress--indeterminate` la barra recorre el carril en bucle: es la
  fase en la que el servidor trabaja y **no hay porcentaje** (se retira
  `aria-valuenow` y se muestra «En curso»). Con movimiento reducido se
  dibuja fija al 100 % atenuada.
- El progreso de **subida** es real: `fetch` no lo expone, así que la carga
  de archivos usa `XMLHttpRequest` y su evento `upload.progress`.
- El texto de la fase va en `.progress-area__label`; el detalle del renglón
  en `.progress-area__detail`.

---

## 5. JavaScript compartido (`window.PV`)

`app.js` se carga **sin `defer`**; los scripts de cada módulo **con `defer`**.
Ese orden es deliberado: `PV` debe existir cuando corran los scripts de página.

| API | Para qué |
|---|---|
| `PV.api(url, options)` | Petición con CSRF y manejo de error uniforme |
| `PV.get/post/put/patch/delete` | Atajos de `PV.api` |
| `PV.toast(msg, tipo, título)` | Aviso efímero |
| `PV.confirm(opts)` | Confirmación modal |
| `PV.openModal/closeModal(id)` | Modales, con foco atrapado |
| `PV.setBusy(botón, busy, texto)` | Estado de espera |
| `PV.showErrors(form, fields)` / `PV.clearErrors(form)` | Errores de validación |
| `PV.serialize(form)` | Formulario → objeto |
| `PV.DataTable(opts)` | Tabla con búsqueda, orden, paginación y esqueleto |
| `PV.SearchSelect(opts)` | Selector con búsqueda asíncrona |
| `PV.setTheme/toggleTheme` | Tema claro/oscuro |
| `PV.setDensity/toggleDensity` | Densidad de tabla (cookie + `localStorage`) |
| `PV.formatMoney/formatNumber/formatDate/formatDateTime` | Formato en cliente |
| `PV.escapeHtml`, `PV.debounce`, `PV.initials` | Utilidades |

Reglas:

- Un módulo **no edita** `public/assets/css/app.css` ni `public/assets/js/app.js`.
  Si necesita un estilo nuevo, se añade al CSS (sección correspondiente) y
  se documenta aquí.
- Todo `fetch` pasa por `PV.api` (CSRF, errores y JSON uniformados).
- Toda salida de texto dinámico pasa por `PV.escapeHtml`.

---

## 6. Iconografía

```php
<?= Icon::render('wallet') ?>            <!-- 18 px por defecto -->
<?= Icon::render('wallet', 'mi-clase', 24) ?>
```

SVG en línea, trazo 1.75, `currentColor`, `aria-hidden="true"`.
**Siempre emite `width`/`height`**: un SVG sin dimensiones se renderiza a
300 × 150.

Nombres disponibles: `dashboard, truck, building, wallet, file-text, package,
chart, users, shield, scroll, settings, log-out, sun, moon, menu,
chevron-down, chevron-left, chevron-right, search, plus, edit, trash, eye,
download, printer, check, check-circle, x, x-circle, alert, info, calendar,
filter, refresh, copy, more, dollar, activity, hash, clock, lock, mail, phone,
map-pin, file-check, layers, inbox, menu-bars, upload, save, link, external,
database, trending-up, trending-down, minus`.

Si falta un icono, se **añade al array `PATHS`** de `app/Support/Icon.php`
(trazo Lucide) y se documenta en esta lista.

---

## 7. Accesibilidad (WCAG 2.2 AA)

- Cuerpo de texto ≥ 15 px; controles de formulario 16 px.
- Contraste: texto principal sobre superficie ≥ 4.5:1; texto grande y
  elementos gráficos ≥ 3:1.
- **Objetivos táctiles ≥ 24 px**; en punteros gruesos (`pointer: coarse`) los
  botones suben a 44 px.
- Todo control interactivo tiene nombre accesible: `aria-label` si es solo
  icono, `<label for>` si tiene etiqueta visible.
- Foco visible siempre (`:focus-visible` con `--focus-ring`); nunca
  `outline: none` sin sustituto.
- `prefers-reduced-motion: reduce` desactiva transiciones y animaciones.
- Modales: `role="dialog"`, `aria-modal="true"`, foco atrapado y devuelto.
- Tablas: `<caption>` o `aria-label`; cabeceras `<th scope="col">`.
- Formularios: errores asociados al campo y anunciados; no solo por color.

---

## 8. Adaptación a pantallas pequeñas

Puntos de corte en `app.css`:

| Anchura | Qué cambia |
|---|---|
| ≤ 1280 px | Rejillas de KPIs a 3 columnas, toolbar se apila |
| ≤ 1080 px | Contenido a 2 columnas, se ocultan columnas secundarias |
| ≤ 900 px | Barra lateral plegable, tabla con scroll horizontal |
| ≤ 640 px | Una columna, botones a ancho completo, paginación centrada |

- La barra lateral se pliega con `PV.toggleSidebar()`.
- El control de densidad pasa al menú del usuario por debajo de 640 px.
- Ninguna tabla debe provocar scroll horizontal de la **página**: el scroll va
  dentro del contenedor `.table-wrapper`.

---

## 9. Checklist para un módulo nuevo

1. La vista usa **solo** clases existentes de `app.css`.
2. Cabecera con `page-header`, `breadcrumb` y una sola acción primaria.
3. Los formularios usan `form-field` + `form-label` + `form-hint`.
4. Los importes se muestran con `tabular` y alineados a la derecha.
5. Los estados se muestran con `badge`; los avisos, con `alert` o `toast`.
6. El botón de icono lleva `aria-label` y `title`.
7. Hay estado vacío diseñado (`.empty-state`) y estado de carga (`.setBusy`).
8. Se comprueba en claro y en oscuro, y a 375 / 768 / 1440 px.
9. El script del módulo va con `defer` y usa `PV.api` / `PV.escapeHtml`.
10. Las acciones respeta permisos con `Auth::can()` **y** con `data-*` en el
    frontend, sin duplicar la lógica de negocio.

---

## 10. Lo que no se hace

- No se fija un color, radio o sombra en una vista: se usa un token.
- No se redefine `app.css` ni `app.js` desde un módulo.
- No se depende de fuentes web ni de librerías de iconos.
- No se cambian consultas, endpoints ni reglas presupuestales por motivos
  visuales.
- No se borran registros con dependencias: se desactivan (regla del
  proyecto).
