@higinio/design-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -4
- package/README.md +11 -8
- package/assets/bruma/design-doc.md +1083 -0
- package/assets/bruma/framework.css +1374 -0
- package/assets/bruma/styleguide.html +4687 -0
- package/assets/bruma/tailwind.css +121 -0
- package/assets/bruma/tokens.css +147 -0
- package/assets/bruma/tokens.json +995 -0
- package/assets/celeste/design-doc.md +183 -17
- package/assets/celeste/framework.css +440 -3
- package/assets/celeste/styleguide.html +1534 -64
- package/assets/celeste/tailwind.css +4 -0
- package/assets/nocturna/design-doc.md +237 -47
- package/assets/nocturna/framework.css +429 -15
- package/assets/nocturna/styleguide.html +1521 -137
- package/assets/nocturna/tailwind.css +8 -2
- package/assets/nocturna/tokens.css +22 -11
- package/assets/nocturna/tokens.json +26 -1
- package/assets/salvia/design-doc.md +1067 -0
- package/assets/salvia/framework.css +1370 -0
- package/assets/salvia/styleguide.html +4670 -0
- package/assets/salvia/tailwind.css +121 -0
- package/assets/salvia/tokens.css +143 -0
- package/assets/salvia/tokens.json +1023 -0
- package/dist/cli.js +7 -5
- package/dist/doc.js +18 -4
- package/dist/preflight.js +2 -1
- package/dist/server.js +7 -7
- package/dist/slop.js +3 -3
- package/dist/systems.js +27 -1
- package/dist/validate.js +13 -5
- package/package.json +3 -3
|
@@ -114,4 +114,8 @@
|
|
|
114
114
|
--color-accent-soft: var(--accent-soft);
|
|
115
115
|
--color-accent-foreground: var(--text-on-primary);
|
|
116
116
|
--color-link: var(--link);
|
|
117
|
+
--color-success: var(--success-500);
|
|
118
|
+
--color-warning: var(--warning-500);
|
|
119
|
+
--color-danger: var(--danger-500);
|
|
120
|
+
--color-info: var(--info-500);
|
|
117
121
|
}
|
|
@@ -48,8 +48,11 @@
|
|
|
48
48
|
| `accent-300` | `#7dccf1` | Iconos activos, gráficas secundarias, código inline |
|
|
49
49
|
| `accent-400` | `#52baed` | **Acento principal** — CTAs, links, focus, datos clave |
|
|
50
50
|
| `accent-500` | `#2fa3dd` | Hover de botones de acento |
|
|
51
|
-
| `accent-600` | `#1f87bb` | Pressed |
|
|
52
|
-
| `accent-700` | `#186a94` | Bordes de elementos activos |
|
|
51
|
+
| `accent-600` | `#1f87bb` | Pressed; en light, **solo UI** (focus, bordes, iconos) — 4.01:1 sobre blanco |
|
|
52
|
+
| `accent-700` | `#186a94` | Bordes de elementos activos; en light, **texto de acción** — 5.95:1 sobre blanco |
|
|
53
|
+
| `accent-800` | `#12506f` | Solo light: hover del texto de acción — 8.74:1 sobre blanco |
|
|
54
|
+
|
|
55
|
+
> ⚠️ **Regla AA del acento en claro:** `accent-600` (`#1f87bb`) sobre blanco da **4.01:1**, no 4.5. Pasa el mínimo de **componentes UI** (3:1) pero **no el de texto** (4.5:1). Todo texto de acción sobre fondo claro usa `accent-700`; su hover, `accent-800`. `accent-400` jamás como texto en claro (2.19:1).
|
|
53
56
|
|
|
54
57
|
### 3.2 Secundario — Índigo Profundo (base `#353d7c`)
|
|
55
58
|
|
|
@@ -70,8 +73,9 @@
|
|
|
70
73
|
| `night-900` | `#0b0d12` | Superficie 1 (tarjetas) |
|
|
71
74
|
| `night-800` | `#14171f` | Superficie 2 (modales, dropdowns, inputs) |
|
|
72
75
|
| `night-700` | `#1e222d` | Superficie 3, hover de filas |
|
|
73
|
-
| `night-600` | `#2a2f3d` |
|
|
76
|
+
| `night-600` | `#2a2f3d` | Superficie de separadores sutiles |
|
|
74
77
|
| `night-500` | `#3d4354` | Bordes por defecto |
|
|
78
|
+
| `night-450` | `#5f6575` | Solo light: texto atenuado (`--text-muted`) — `night-400` caía a 4.27:1 sobre `--surface-3` |
|
|
75
79
|
| `night-400` | `#6b7183` | Deshabilitado, placeholders |
|
|
76
80
|
| `night-300` | `#9aa0b0` | Texto terciario, captions |
|
|
77
81
|
| `night-200` | `#c5c9d4` | Texto secundario |
|
|
@@ -98,7 +102,7 @@ Los componentes **nunca** usan los primitivos de arriba directamente: consumen e
|
|
|
98
102
|
| `--surface-2` | `night-800` | Modales, dropdowns, inputs |
|
|
99
103
|
| `--surface-3` | `night-700` | Hover de filas, superficie máxima |
|
|
100
104
|
| `--border` | `night-500` | Borde por defecto |
|
|
101
|
-
| `--border-strong` | `night-
|
|
105
|
+
| `--border-strong` | `night-400` | Borde de énfasis (elevación 3, accordion). **Mismo valor en ambos temas**: un gris medio cumple 1.4.11 sobre fondos oscuros (3.26-3.99:1) y claros (4.27-4.87:1). En dark **sube** de peldaño respecto a `--border` (night-500) porque oscurecerlo lo debilitaría (principio 2) |
|
|
102
106
|
| `--text-primary` | `night-100` | Texto principal |
|
|
103
107
|
| `--text-secondary` | `night-200` | Texto secundario |
|
|
104
108
|
| `--text-muted` | `night-300` | Terciario, captions |
|
|
@@ -206,7 +210,7 @@ Tokens compuestos:
|
|
|
206
210
|
|
|
207
211
|
```css
|
|
208
212
|
--border-default: 1px solid var(--night-500);
|
|
209
|
-
--border-emphasis: 1px solid var(--night-
|
|
213
|
+
--border-emphasis: 1px solid var(--night-400);
|
|
210
214
|
--focus-ring: 2px solid var(--accent-400);
|
|
211
215
|
--focus-ring-offset: 2px;
|
|
212
216
|
```
|
|
@@ -224,7 +228,7 @@ En fondo casi negro las sombras solas no bastan: la elevación combina **superfi
|
|
|
224
228
|
| 0 | `night-950` | — | Fondo de la app |
|
|
225
229
|
| 1 | `night-900` | `--shadow-1: 0 1px 2px rgba(0,0,0,.4)` | Tarjetas en reposo |
|
|
226
230
|
| 2 | `night-800` | `--shadow-2: 0 4px 12px rgba(0,0,0,.5)` | Dropdowns, hover |
|
|
227
|
-
| 3 | `night-800` + borde `night-
|
|
231
|
+
| 3 | `night-800` + borde `night-400` | `--shadow-3: 0 12px 32px rgba(0,0,0,.6)` | Modales, popovers |
|
|
228
232
|
|
|
229
233
|
Tokens adicionales:
|
|
230
234
|
|
|
@@ -304,6 +308,7 @@ Indicador de carga circular; también sustituye el label del botón en estado lo
|
|
|
304
308
|
|
|
305
309
|
- Fondo `night-800`, borde `1px night-500`, radius `sm`, altura 44px, padding `0 14px`, texto `night-100`.
|
|
306
310
|
- **Label:** `text-sm` peso 500 `night-200`, margen inferior 6px. Siempre visible (nunca placeholder-como-label).
|
|
311
|
+
- **Campo obligatorio (`.field--required`):** el label muestra un asterisco `danger-400` **decorativo** — pseudo-elemento (`aria-hidden` implícito), nunca un carácter que el lector anuncie. La obligatoriedad real se comunica en el control con `required` + `aria-required="true"`; el asterisco es solo un refuerzo visual (estado nunca solo por color). Contrapartida: marca lo **opcional** con "(opcional)" si el formulario tiene mayoría de campos obligatorios.
|
|
307
312
|
- **Placeholder:** `night-400`.
|
|
308
313
|
- **Hover:** borde `night-400`.
|
|
309
314
|
- **Focus:** borde `accent-400` + `--ring`. Transición 150ms.
|
|
@@ -312,13 +317,13 @@ Indicador de carga circular; también sustituye el label del botón en estado lo
|
|
|
312
317
|
- **Disabled:** fondo `night-900`, texto `night-400`, cursor `not-allowed`.
|
|
313
318
|
- **Ayuda:** `text-sm` `night-300` debajo.
|
|
314
319
|
- Icono leading: padding-left 40px, icono 18px `night-400`.
|
|
315
|
-
- **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames.
|
|
320
|
+
- **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames; **`autocomplete` apropiado** (`email`, `current-password`, `one-time-code`, `name`…) para el autorrelleno del navegador.
|
|
316
321
|
- **Nunca bloquear el pegado** (`onpaste`): rompe gestores de contraseñas y códigos de verificación.
|
|
317
322
|
- **Al enviar con errores,** mover el foco al primer campo inválido (no solo pintarlo de rojo).
|
|
318
323
|
|
|
319
324
|
### 10.5 Textarea
|
|
320
325
|
|
|
321
|
-
Mismos estados que input. Min-height 96px, `resize: vertical`. Contador `text-xs`
|
|
326
|
+
Mismos estados que input. Min-height 96px, `resize: vertical`. Contador (`.field__count`) `text-xs` `--text-muted` a la derecha.
|
|
322
327
|
|
|
323
328
|
### 10.6 Select
|
|
324
329
|
|
|
@@ -349,6 +354,7 @@ Selector de valor único en un rango (`min`/`max`/`step`/`value`).
|
|
|
349
354
|
|
|
350
355
|
- 20px, borde `2px night-500`, radius 4px / full.
|
|
351
356
|
- Checked: fondo `accent-400`, check `night-950` animado (stroke-dashoffset, 200ms).
|
|
357
|
+
- **Indeterminate (`input:indeterminate`):** tercer estado, no un "checked a medias" — un checkbox maestro cuyos hijos están parcialmente marcados. Se pinta con fondo `accent-400` y un **guion horizontal** `text-on-accent` (2px de alto), nunca el check. Solo puede activarse por JS (`el.indeterminate = true`); al marcarlo/desmarcarlo el maestro pasa a checked/unchecked normal.
|
|
352
358
|
- Hover: borde `accent-300`. Focus: ring como inputs.
|
|
353
359
|
- Label clicable, gap 10px.
|
|
354
360
|
|
|
@@ -393,7 +399,7 @@ Banner de estado **inline y persistente** (a diferencia del Toast, transitorio):
|
|
|
393
399
|
|
|
394
400
|
- 4 tonos: fondo suave semántico (`*-bg`, ej. `info-bg` `#0d1f2c`), borde `1px` del tono al 30%, radius `md`, padding 14px 16px.
|
|
395
401
|
- Icono semántico 20px en el color del tono: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`.
|
|
396
|
-
- Título `text-sm` semibold y cuerpo `text-sm`, ambos en el color del tono (
|
|
402
|
+
- Título `text-sm` semibold y cuerpo `text-sm`, ambos en el color del tono, a contraste completo (sin opacidad).
|
|
397
403
|
- Botón cerrar opcional (× 18px, hit-área 36px) vía `onDismiss`; hover fondo `currentColor` al 14%. `role="status"`.
|
|
398
404
|
|
|
399
405
|
### 10.17 Toast
|
|
@@ -422,9 +428,17 @@ Progreso **determinado** (para carga indeterminada usar `Skeleton`, no Progress)
|
|
|
422
428
|
- Panel: fondo `night-800`, borde `night-600`, radius `lg`, sombra nivel 3, max-width 480/640px, padding 24px.
|
|
423
429
|
- Entrada: fade + scale(0.96→1) 250ms. Salida 180ms.
|
|
424
430
|
- Header `text-xl` + IconButton cerrar; footer con primaria al extremo derecho.
|
|
425
|
-
- Esc y click en overlay cierran (excepto flujos destructivos).
|
|
431
|
+
- Esc y click en overlay cierran (excepto flujos destructivos: marca el overlay con `data-modal-persistent` para exigir una elección explícita — ni Esc ni click fuera lo cierran).
|
|
426
432
|
- `overscroll-behavior: contain` en el panel scrolleable — al llegar al fondo, el scroll no se encadena a la página de atrás.
|
|
427
433
|
|
|
434
|
+
**Accesibilidad y gestión de foco (obligatorias):**
|
|
435
|
+
|
|
436
|
+
- El panel es `role="dialog"` + `aria-modal="true"` + `aria-labelledby` (id del `.modal__title`); si hay un párrafo de descripción, `aria-describedby` con su id.
|
|
437
|
+
- **Al abrir:** guarda el elemento disparador y mueve el foco al panel (`tabindex="-1"`) o al primer control — así el lector anuncia el título; el panel no muestra anillo porque el foco es programático (`:focus-visible` no se dispara).
|
|
438
|
+
- **Focus-trap:** Tab desde el último control vuelve al primero y Shift+Tab desde el primero salta al último; el foco nunca escapa al fondo mientras el modal está abierto.
|
|
439
|
+
- **Al cerrar:** devuelve el foco al disparador que lo abrió (retorno de foco), venga el cierre de Esc, del overlay o de un botón `data-close-modal`.
|
|
440
|
+
- Motion sin cambios: overlay fade+blur 200ms, panel fade + `scale(0.96→1)` 250ms; ambos respetan `prefers-reduced-motion` vía el piso global.
|
|
441
|
+
|
|
428
442
|
### 10.20 Menu
|
|
429
443
|
|
|
430
444
|
Lista de acciones desplegable anclada a un trigger (acciones de fila, botón "más", menú de cuenta) — mismo patrón visual que el menú de `Select`, pero se abre desde cualquier trigger.
|
|
@@ -436,18 +450,41 @@ Lista de acciones desplegable anclada a un trigger (acciones de fila, botón "m
|
|
|
436
450
|
|
|
437
451
|
### 10.21 Tabla
|
|
438
452
|
|
|
439
|
-
- Header: fondo `night-900`, `text-xs` uppercase `+0.08em` `night-300`, borde inferior `night-600`.
|
|
453
|
+
- Header: fondo `night-900`, `text-xs` uppercase `+0.08em` `night-300`, borde inferior `night-600`. Siempre `scope="col"` en los `<th>`.
|
|
440
454
|
- Filas: altura 52px, borde inferior `night-800`, hover fondo `night-700`.
|
|
441
455
|
- Numéricos a la derecha con `tabular-nums`.
|
|
442
|
-
-
|
|
456
|
+
- **Zebra (`.table--zebra`):** filas pares en `--surface-2`; el hover (`--surface-3`) y la selección mandan sobre la banda. Úsala en tablas anchas y densas para seguir la fila; una tabla corta no la necesita.
|
|
457
|
+
- **Sticky header (`.table--sticky`):** `thead` con `position: sticky; top: 0` para que los encabezados queden fijos al hacer scroll dentro de un contenedor con altura acotada (`overflow: auto`).
|
|
458
|
+
- **Header ordenable:** el `<th>` contiene `<button class="th-sort">` con el label + `<svg class="sort-icon">`; el estado vive en `aria-sort` del `<th>` (`none` | `ascending` | `descending`). El icono está atenuado cuando la columna no ordena y se tiñe de `accent-400` (rotando 180° para descendente) cuando sí. Una sola columna ordenada a la vez.
|
|
459
|
+
- **Selección (`.row--selected`):** columna con checkbox en el `<th>` (maestro, con `aria-label`) y en cada fila (checkbox con `aria-label` que describe la fila). La fila marcada usa `.row--selected`: fondo `--accent-selected` + borde izquierdo 2px `accent-400` — no depende solo del color, el checkbox marcado lo refuerza.
|
|
443
460
|
|
|
444
461
|
### 10.22 Stat
|
|
445
462
|
|
|
446
463
|
- **Label** overline arriba: 12px, peso 500, uppercase, tracking `+0.08em`, `night-300`.
|
|
447
464
|
- **Cifra:** JetBrains Mono 49px peso 500, line-height 1.05, color `accent-400`, `tabular-nums`, `white-space: nowrap`.
|
|
448
465
|
- **Delta** opcional debajo: mono 14px — `success-400` / `danger-400` / `night-300` (neutral).
|
|
449
|
-
- Gap vertical 6px. Formato numérico `es-
|
|
450
|
-
- **Count-up**
|
|
466
|
+
- Gap vertical 6px. Formato numérico con `Intl.NumberFormat` (`es-MX`).
|
|
467
|
+
- **Count-up** 350ms ease-out al montar (techo `--duration-page`); con `prefers-reduced-motion` muestra el valor directo.
|
|
468
|
+
|
|
469
|
+
#### Chip de métrica
|
|
470
|
+
|
|
471
|
+
Pastilla de una línea con rótulo y valor —por ejemplo, «Agua **3 de 8**»— para métricas secundarias que no merecen una cifra de Stat.
|
|
472
|
+
|
|
473
|
+
- **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface-1`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
|
|
474
|
+
- **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va en JetBrains Mono peso 500, `--link` y `tabular-nums`.
|
|
475
|
+
- **Fila:** `.metric-chips` agrupa los chips con `display: flex; flex-wrap: wrap; gap: var(--space-2)`, así que saltan de línea en móvil.
|
|
476
|
+
- **Interactivo** (por ejemplo, «sumar un vaso»): `button.metric-chip`, con `min-height: var(--touch-target)` (44px) y padding horizontal de 16px. Hover: fondo `--outline-hover-bg` y borde `--border-hover` (como Outline). Active: fondo `--outline-active-bg` y `scale(.98)`. Foco de teclado con el anillo del sistema (`--focus-ring`, offset `--focus-ring-offset`).
|
|
477
|
+
|
|
478
|
+
```html
|
|
479
|
+
<div class="metric-chips">
|
|
480
|
+
<span class="metric-chip"><span class="metric-chip__label">Latencia</span> <span class="metric-chip__value">42 ms</span></span>
|
|
481
|
+
<button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
|
|
482
|
+
<span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Accesibilidad:** el chip interactivo es un `<button>` con nombre accesible completo, no solo la cifra («Agua: 3 de 8 vasos. Sumar un vaso»); el nombre empieza por el texto visible (WCAG 2.5.3) y se actualiza con el valor. El chip informativo es texto: no lleva rol ni foco.
|
|
451
488
|
|
|
452
489
|
### 10.23 Pagination
|
|
453
490
|
|
|
@@ -465,6 +502,37 @@ Navegador de páginas 1-indexado, bajo tablas y listas largas.
|
|
|
465
502
|
- **Tabs:** borde inferior `night-600`; indicador 2px `accent-400` que se desliza (250ms).
|
|
466
503
|
- **Breadcrumbs:** `text-sm` `night-300`, separador `/` (tipográfico), último ítem `night-100`.
|
|
467
504
|
|
|
505
|
+
#### Sidebar contraíble
|
|
506
|
+
|
|
507
|
+
Variante del Sidebar para consolas y dashboards con muchas secciones: alterna entre la lista con rótulos y un riel de iconos que deja más ancho a los datos.
|
|
508
|
+
|
|
509
|
+
- **Estados:** expandido, 260px (el ancho del Sidebar), y contraído, riel de iconos de 64px. Clases: `.sidebar.sidebar--collapsible` y, contraído, `.is-collapsed`. Cada ítem lleva su icono de 18px y el nombre en `.sidebar__label`.
|
|
510
|
+
- **Grupos con rótulo:** `.sidebar__group` con un `.sidebar__group-label` (overline: 12px, peso 500, mayúsculas, tracking `+0.08em`, `--text-muted`); 16px entre grupos. En el riel, cada rótulo se convierte en un separador de 1px `--border` y el del primer grupo se oculta.
|
|
511
|
+
- **Ítem activo:** `.is-active` (fondo `--sidebar-active-bg`, texto `--sidebar-active-text`, icono `--sidebar-active-icon`) más `aria-current="page"`.
|
|
512
|
+
- **Botón de contraer:** `.sidebar__item.sidebar__toggle` al pie (`margin-top: auto`), con un chevron que apunta hacia donde se moverá el borde (rota 180° en el riel) y el texto «Contraer» / «Expandir» en su `.sidebar__label`.
|
|
513
|
+
- **Tooltip en el riel:** el `.sidebar__label` queda oculto a la vista pero accesible (patrón de `.visually-hidden`) y aparece a la derecha del ítem con el look del Tooltip (§10.15: `--tooltip-bg`, borde `--tooltip-border`, `text-xs`, delay 400ms) al pasar el cursor —solo con puntero real, §11.5— y al enfocar el ítem con teclado (`:focus-visible`). No es un `title`: el nombre sigue en el árbol de accesibilidad. El riel no recorta su contenido (sin `overflow`) para que el tooltip pueda salir de él.
|
|
514
|
+
- **Por debajo de 768px** (`md`), `.sidebar--collapsible` no se muestra fijo: lo sustituye un botón ☰ `.icon-btn.sidebar-trigger` que abre un Drawer `.drawer--left` (§10.29) con los mismos grupos e ítems. El framework muestra `.sidebar-trigger` solo por debajo de 768px y lo oculta (`display: none`) desde 768px, el mismo breakpoint en el que aparece el sidebar fijo; así el ☰ y el sidebar nunca conviven. El botón lleva `aria-label="Abrir navegación"`, `aria-controls` con el id del drawer y `aria-expanded` (`false` cerrado, `true` abierto).
|
|
515
|
+
- **Motion:** el ancho cambia sin transición —el layout usa `grid-template-columns: auto 1fr` y el contenido sigue al sidebar al instante—; nunca se anima `width`. Solo los rótulos se funden al expandir: `opacity` 0→1 y `translateX(-4px)`→0 en `--duration-fast` `--ease-out`. Con `prefers-reduced-motion`, sin animación (piso global, §11.5).
|
|
516
|
+
|
|
517
|
+
```html
|
|
518
|
+
<nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
|
|
519
|
+
<div class="sidebar__group" role="group" aria-labelledby="navMonitor">
|
|
520
|
+
<span class="sidebar__group-label" id="navMonitor">Monitoreo</span>
|
|
521
|
+
<a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
|
|
522
|
+
</div>
|
|
523
|
+
<button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
|
|
524
|
+
<svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
|
|
525
|
+
</button>
|
|
526
|
+
</nav>
|
|
527
|
+
|
|
528
|
+
<!-- Solo por debajo de 768px -->
|
|
529
|
+
<button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
|
|
530
|
+
aria-expanded="false" aria-controls="navDrawer">…menu…</button>
|
|
531
|
+
<div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Accesibilidad:** el contenedor es un `<nav>` con `aria-label`; el ítem de la página actual lleva `aria-current="page"` además de `.is-active`. El toggle es un `<button>` real con `aria-expanded` (`true` expandido, `false` en el riel) y `aria-controls` con el id del sidebar; al alternar, su texto cambia entre «Contraer» y «Expandir». El ☰ `.sidebar-trigger` también es un `<button>` con `aria-expanded` y `aria-controls` hacia el drawer, y el drawer sigue el contrato de foco del Modal. Los grupos se declaran con `role="group"` y `aria-labelledby` apuntando a su rótulo, que en el riel sigue siendo legible para el lector aunque se vea como una línea.
|
|
535
|
+
|
|
468
536
|
### 10.25 Skeleton y EmptyState
|
|
469
537
|
|
|
470
538
|
- Skeleton: fondo `night-800` con shimmer (gradiente hacia `night-700`, barre en 1.4s loop). Radius igual al contenido.
|
|
@@ -478,6 +546,91 @@ Navegador de páginas 1-indexado, bajo tablas y listas largas.
|
|
|
478
546
|
- **Link:** color `accent-400`; hover `accent-200` (150ms). Subrayado animado: línea 1px `currentColor` a -2px, `scaleX(0)→scaleX(1)` desde la izquierda, 200ms ease-out. Externo: `target="_blank" rel="noopener noreferrer"` + icono flecha 12px, gap 4px.
|
|
479
547
|
- **Code (inline):** JetBrains Mono `0.9em`, texto `accent-300`, fondo `night-800`, borde `1px night-700`, radius 6px, padding `1px 6px`.
|
|
480
548
|
|
|
549
|
+
### 10.27 AvatarGroup
|
|
550
|
+
|
|
551
|
+
Pila de avatares solapados para representar un conjunto de colaboradores/participantes (complementa §10.12 Avatar).
|
|
552
|
+
|
|
553
|
+
- **`.avatar-group`:** contenedor `inline-flex`; cada avatar reutiliza `.avatar` (cualquier tamaño) y se solapa 8px (`margin-left: -8px`, salvo el primero).
|
|
554
|
+
- **Borde separador:** cada avatar del grupo lleva un borde `2px` **del color del fondo** — `--surface-1` sobre tarjetas, `--bg` sobre el lienzo — para recortarse del de atrás. Es el único caso donde el borde del avatar cambia de color respecto a §10.12.
|
|
555
|
+
- **`.avatar-group__more`:** chip `+N` (modificador sobre `.avatar`) con fondo neutro `--surface-3` y texto `--text-secondary` en Manrope (sin uppercase ni tracking de display), para los avatares ocultos. Va siempre al final.
|
|
556
|
+
- **Accesibilidad:** dale al grupo un nombre accesible (`aria-label="Equipo del proyecto"` o texto adyacente); los avatares con imagen conservan su `alt`. El `+N` no es interactivo por defecto; si abre una lista, conviértelo en `<button>` con `aria-label` ("Ver 3 personas más").
|
|
557
|
+
|
|
558
|
+
### 10.28 Combobox / Autocomplete y Search
|
|
559
|
+
|
|
560
|
+
Dos patrones de entrada con filtrado. Complementan al `Select` (§10.6): el combobox se escribe y filtra en vivo; el search recorre contenido.
|
|
561
|
+
|
|
562
|
+
**Combobox (`.combobox`):**
|
|
563
|
+
|
|
564
|
+
- `.combobox__input` reutiliza el `Input` (§10.4); `.combobox__list` es el desplegable sobre `--surface-2` (superficie de dropdowns), borde `--border-strong`, sombra nivel 3, radius `md`; `.combobox__option` es cada opción.
|
|
565
|
+
- **Dos estados distintos, nunca solo por color:** `.is-active` = la opción resaltada por teclado (fondo `--surface-3`); `[aria-selected="true"]` = la opción ya elegida (fondo `--accent-selected` + texto `accent-300` + icono check en `accent-400`). La elegida se refuerza con el icono, no solo con el tinte.
|
|
566
|
+
- **Accesibilidad:** el input es `role="combobox"` + `aria-expanded` + `aria-controls` (id de la lista) + `aria-autocomplete="list"` + `aria-activedescendant` (id de la opción activa). La lista es `role="listbox"` con id; cada opción `role="option"` + `aria-selected` + id. `autocomplete="off"` para no chocar con el autorrelleno del navegador.
|
|
567
|
+
- **Teclado:** ↑/↓ mueven la opción activa, Enter la selecciona, Esc cierra. El foco nunca sale del input (patrón listbox); las opciones no son tabulables.
|
|
568
|
+
|
|
569
|
+
**Search (`.search`):**
|
|
570
|
+
|
|
571
|
+
- Wrapper `.search` con `.search__icon` (lupa, `aria-hidden`), `.search__input` (reutiliza el `Input`, con padding para el icono y el botón) y `.search__clear` (× con `aria-label="Limpiar"`, visible solo cuando hay texto).
|
|
572
|
+
- El botón limpiar vacía el campo y devuelve el foco al input. Se oculta el botón nativo de `type="search"` de WebKit para no duplicar el affordance.
|
|
573
|
+
|
|
574
|
+
### 10.29 Drawer / Sheet
|
|
575
|
+
|
|
576
|
+
Panel que entra deslizándose desde un borde de la pantalla — filtros, detalle contextual, navegación en móvil. Mismo contrato de foco que el `Modal` (§10.19).
|
|
577
|
+
|
|
578
|
+
- **`.drawer`** (contenedor `fixed`), **`.drawer__overlay`** (mismo `--overlay` del modal, **sin blur nuevo** — el blur solo existe en navbar y overlay de modal, §7), **`.drawer__panel`** sobre `--surface-2` con borde `--border-strong` y sombra nivel 3.
|
|
579
|
+
- **Variantes:** `.drawer--right` (default), `.drawer--left`, `.drawer--bottom` (sheet, con radius superior).
|
|
580
|
+
- **Accesibilidad y foco (obligatorios):** panel `role="dialog"` + `aria-modal="true"` + `aria-labelledby`; **focus-trap + retorno de foco** idénticos al Modal (guarda el disparador al abrir, lo restaura al cerrar); cierra con Esc y con clic en el overlay. `overscroll-behavior: contain` en el panel para que el scroll no se encadene a la página de atrás.
|
|
581
|
+
- **Motion:** `transform: translateX(100%)→0` (translateY para `--bottom`) + fade del overlay, `--duration-slow`/`--ease-out`, dentro del techo (350ms). Entrada y salida interrumpibles vía la clase `.is-open` (`transition`, no keyframes); respetan `prefers-reduced-motion` por el piso global.
|
|
582
|
+
|
|
583
|
+
### 10.30 Popover
|
|
584
|
+
|
|
585
|
+
Panel flotante anclado a un disparador que se abre por **clic** — a diferencia del `Tooltip` (§10.15), que aparece por hover y solo informa. El popover puede contener controles (enlaces, botones, campos).
|
|
586
|
+
|
|
587
|
+
- **`.popover`** (ancla `relative`), **`.popover__trigger`** (cualquier botón), **`.popover__panel`** sobre `--surface-2`, borde `--border-strong`, sombra nivel 3, radius `md`. Origin-aware: crece desde el `transform-origin` del disparador (`--end` lo alinea a la derecha).
|
|
588
|
+
- **Accesibilidad:** trigger `aria-haspopup="dialog"` + `aria-expanded`; panel `role="dialog"` + `aria-label`. El foco entra al panel al abrir; cierra con Esc (devuelve el foco al trigger) y con clic fuera.
|
|
589
|
+
- **Motion:** fade + `scale(0.96→1)` desde el origen, `--duration-base`/`--ease-out`. La visibilidad se retrasa hasta el final de la salida (`visibility`), así el panel cerrado no queda en el orden de tabulación.
|
|
590
|
+
|
|
591
|
+
### 10.31 ButtonGroup
|
|
592
|
+
|
|
593
|
+
Botones adyacentes que se leen como un control agrupado de acciones (no de selección — para selección excluyente usa `SegmentedControl`, §10.7).
|
|
594
|
+
|
|
595
|
+
- **`.btn-group`** agrupa `.btn` existentes: el radius queda solo en los extremos y los bordes internos se colapsan (`margin-left: -1px`). El botón en hover/foco sube de `z-index` para que su borde no lo tape el vecino.
|
|
596
|
+
- **Accesibilidad:** contenedor `role="group"` + `aria-label` que nombra el conjunto.
|
|
597
|
+
- Funciona con cualquier variante de `.btn`; lo habitual es `outline` para que los bordes compartidos se lean.
|
|
598
|
+
|
|
599
|
+
### 10.32 DatePicker
|
|
600
|
+
|
|
601
|
+
Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, se elige un día y el valor vuelve al campo.
|
|
602
|
+
|
|
603
|
+
- **`.datepicker`** (ancla `relative`), **`.datepicker__input`** (reutiliza el `Input` §10.4, con icono de calendario leading vía `.input-wrap`), **`.datepicker__calendar`** = panel que reutiliza el look de `.popover__panel` sobre `--surface-2` (borde `--border-strong`, sombra nivel 3, radius `md`).
|
|
604
|
+
- **Calendario:** `.calendar` contiene `.calendar__header` (mes y año + prev/next con los iconos chevron-left/chevron-right), `.calendar__weekdays` (iniciales L–D) y `.calendar__grid` con los `.calendar__day`.
|
|
605
|
+
- **Estados del día:** `.calendar__day--today` (aro interior `accent-400` + texto `accent-300`), `.calendar__day--selected` (fondo `accent-400`, texto `--text-on-accent`, peso 600 — no depende solo del color), `.calendar__day--outside` (días del mes vecino, `--text-muted`) y `[disabled]` (fuera de rango).
|
|
606
|
+
- **Accesibilidad:** `.calendar__table` es `role="grid"` (con `aria-labelledby` al título del mes) y envuelve la fila de cabeceras `.calendar__weekdays` (`role="row"`) y `.calendar__grid` (`role="rowgroup"`) con filas `.calendar__week` (`role="row"`, con `display: contents` para no romper el grid); el input lleva `aria-haspopup="dialog"` + `aria-controls`, sin `aria-expanded` (no es válido en un `input` de texto); los encabezados de día son `role="columnheader"` con `aria-label` del nombre completo (lunes…); cada día es un `<button role="gridcell">` con `aria-label` de la fecha completa ("10 de julio de 2026") y `aria-selected`. El día de hoy lleva `aria-current="date"`.
|
|
607
|
+
- **Teclado:** el input abre el calendario (clic) y mueve el foco al día activo; las flechas mueven el día enfocado (±1 día, ±7 una semana) cruzando meses, Inicio/Fin saltan a los extremos de la semana, Enter/Espacio eligen y Esc cierra devolviendo el foco al input. Roving tabindex: solo el día activo es tabulable.
|
|
608
|
+
|
|
609
|
+
### 10.33 FileUpload
|
|
610
|
+
|
|
611
|
+
Zona de arrastre con lista de archivos.
|
|
612
|
+
|
|
613
|
+
- **`.file-upload`** envuelve el `.file-upload__input` (file real, oculto accesiblemente con `.visually-hidden` — sigue en el orden de tabulación) y el `.file-upload__label` (zona clicable, borde discontinuo). Dentro del label, `.file-upload__button` resalta la palabra "explora" en `--link`, y `.file-upload__hint` da formatos y tamaño.
|
|
614
|
+
- **`.file-upload.is-dragover`:** al arrastrar un archivo encima, el borde pasa a `accent-400` y el fondo a `--accent-ghost-hover`.
|
|
615
|
+
- **Lista:** `.file-upload__list` con un `.file-upload__item` por archivo — icono + `.file-upload__name` + `.file-upload__size` (mono) + `.file-upload__remove` (× con `aria-label`). Un ítem subiendo incrusta el `.progress` (§10.18). El error usa `.file-upload__item--error`: borde y texto en `danger` **con icono de alerta + texto** ("Error: supera los 10 MB") — nunca solo por color.
|
|
616
|
+
- **Accesibilidad:** el input file lleva label asociado (`for`/`id`) y `aria-describedby` que apunta a la ayuda de formatos/tamaño; el anillo de foco se pinta sobre el label (`:focus-visible` del input, oculto). El botón quitar nombra su archivo ("Quitar informe.pdf").
|
|
617
|
+
|
|
618
|
+
### 10.34 Stepper / Wizard
|
|
619
|
+
|
|
620
|
+
Indicador de progreso por pasos.
|
|
621
|
+
|
|
622
|
+
- **`.stepper`** es un `<ol>`; cada `.step` tiene `.step__marker` (círculo con el número, o el icono check cuando está completo), `.step__label` y `.step__connector` (la línea al paso siguiente; el último paso no la lleva).
|
|
623
|
+
- **Estados:** `.step--complete` (marcador con fondo `accent-400` + check en `--text-on-accent`; el conector se tiñe de `accent-400`), `.step--current` (marcador con borde `accent-400` y número en `accent-300`, label en `--text-primary` peso 600) y `.step--upcoming` (marcador y label atenuados).
|
|
624
|
+
- **Variante `.stepper--vertical`:** apila los pasos; el marcador va a la izquierda y el conector es vertical.
|
|
625
|
+
- **Accesibilidad:** al ser un `<ol>` el orden y el conteo se anuncian solos; el paso en curso lleva `aria-current="step"`; los pasos completos muestran el check (estado no solo por color) y refuerzan con texto oculto ("Completado:", "Paso actual:", "Pendiente:"). El conector es decorativo (`aria-hidden`).
|
|
626
|
+
|
|
627
|
+
### 10.35 Kbd
|
|
628
|
+
|
|
629
|
+
Tecla o atajo de teclado.
|
|
630
|
+
|
|
631
|
+
- **`.kbd`** (sobre `<kbd>`): fondo `--surface-2`, borde `--border-strong`, sombra sutil (nivel 1), JetBrains Mono `text-xs`, radius 6px, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal entre ellas (`⌘ + K`).
|
|
632
|
+
- Úsala en tooltips de atajos, menús y documentación; nunca como botón — es informativa, no interactiva.
|
|
633
|
+
|
|
481
634
|
---
|
|
482
635
|
|
|
483
636
|
## 11. Animaciones y motion
|
|
@@ -522,7 +675,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
|
|
|
522
675
|
6. **Origen consciente** — Popovers, menús y dropdowns aparecen desde el `transform-origin` de su disparador; los modales, desde el centro. Nunca un origen arbitrario.
|
|
523
676
|
7. **Interrumpible** — Toda animación puede invertirse desde su valor visible actual, sin saltar al destino y reanimar. Clave en hover y gestos. En la práctica: para UI que se dispara con frecuencia (toasts, dropdowns, hover) usa `transition` — con `@starting-style` para la entrada al montar —, no `@keyframes`: un keyframe no puede invertirse a mitad de camino. Los `@keyframes` quedan para loops de carga (spinner, shimmer).
|
|
524
677
|
8. **La salida es más discreta que la entrada** — Además de ~20% más corta, atenúa: menor desplazamiento y menor opacidad. Entrar se nota; salir se difumina.
|
|
525
|
-
9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs
|
|
678
|
+
9. **El easing según el trabajo** — Entrar → `--ease-out`; salir → `--ease-in`; un elemento que se mueve de un punto a otro de la pantalla (indicador de tabs que se desliza, reorder) → `--ease-in-out`, que acelera y frena en espejo; hover y cambios de color → `--ease-out` corto; loops de carga → `linear`.
|
|
526
679
|
10. **`will-change` solo mientras anima** — Se pone justo antes de animar y se quita al terminar; nunca permanente en el CSS. Presupuesto de capas GPU: menos de 10 elementos promovidos a la vez.
|
|
527
680
|
|
|
528
681
|
### 11.3 Micro-interacciones por componente
|
|
@@ -613,11 +766,13 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
|
|
|
613
766
|
|
|
614
767
|
## 12. Accesibilidad y contraste
|
|
615
768
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
- `night-
|
|
619
|
-
- `
|
|
620
|
-
- `night-
|
|
769
|
+
Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/contraste-tokens.test.ts` los recalcula desde `tokens.css` en cada corrida: si alguien retoca la paleta y rompe un mínimo, la suite falla.
|
|
770
|
+
|
|
771
|
+
- `night-100` sobre `night-950`: **17.25:1** ✅ AAA.
|
|
772
|
+
- `accent-400` sobre `night-950`: **9.50:1** ✅ — sirve como texto/link.
|
|
773
|
+
- `night-950` sobre `accent-400` (CTAs): **9.50:1** ✅ — por eso el texto del CTA es oscuro.
|
|
774
|
+
- `indigo-500` sobre `night-950`: **2.09:1** ⚠️ — solo fondo/decorativo, nunca texto.
|
|
775
|
+
- `night-50` sobre `indigo-500`: **9.29:1** ✅ — texto sobre fondos índigo funciona.
|
|
621
776
|
- Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y UI.
|
|
622
777
|
- Focus visible siempre (`:focus-visible` global, §6); estado nunca solo con color (color + icono + texto).
|
|
623
778
|
- Touch targets 44×44px.
|
|
@@ -652,7 +807,7 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
|
|
|
652
807
|
/* == Aliases semánticos (los componentes consumen SOLO esto) == */
|
|
653
808
|
--bg: var(--night-950);
|
|
654
809
|
--surface-1: var(--night-900); --surface-2: var(--night-800); --surface-3: var(--night-700);
|
|
655
|
-
--border: var(--night-500); --border-strong: var(--night-
|
|
810
|
+
--border: var(--night-500); --border-strong: var(--night-400);
|
|
656
811
|
--text-primary: var(--night-100); --text-secondary: var(--night-200);
|
|
657
812
|
--text-muted: var(--night-300); --text-disabled: var(--night-400);
|
|
658
813
|
--text-heading: var(--night-50); --text-on-accent: var(--night-950);
|
|
@@ -688,7 +843,7 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
|
|
|
688
843
|
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
|
|
689
844
|
--radius-xl: 24px; --radius-full: 9999px;
|
|
690
845
|
--border-default: 1px solid var(--night-500);
|
|
691
|
-
--border-emphasis: 1px solid var(--night-
|
|
846
|
+
--border-emphasis: 1px solid var(--night-400);
|
|
692
847
|
--focus-ring: 2px solid var(--accent-400);
|
|
693
848
|
--focus-ring-offset: 2px;
|
|
694
849
|
|
|
@@ -742,8 +897,8 @@ El sistema es **dark-first**, pero todo producto debe soportar light mode con lo
|
|
|
742
897
|
### 15.1 Filosofía de la inversión
|
|
743
898
|
|
|
744
899
|
- Los fondos claros se tiñen levemente de azul (derivados del acento), nunca gris genérico ni blanco plano en toda la pantalla.
|
|
745
|
-
- El celeste `#52baed` sobre blanco tiene contraste bajo (
|
|
746
|
-
- El CTA conserva la firma de marca: **fondo `#52baed` con texto `#010203`** funciona igual sobre claro (
|
|
900
|
+
- El celeste `#52baed` sobre blanco tiene contraste bajo (2.19:1): **no puede ser texto ni link en light**. El texto de acción baja a `accent-700 (#186a94)` (5.95:1) y su hover a `accent-800 (#12506f)` (8.74:1). `accent-600 (#1f87bb)` **no sirve como texto** —da 4.01:1, por debajo del 4.5 de AA— pero sí como UI (focus, bordes, iconos: mínimo 3:1), y ahí conserva la firma de marca.
|
|
901
|
+
- El CTA conserva la firma de marca: **fondo `#52baed` con texto `#010203`** funciona igual sobre claro (9.50:1) — el botón se ve idéntico en ambos modos.
|
|
747
902
|
- El índigo `#353d7c` gana protagonismo en light: aquí **sí** funciona como texto, headings de marca y fondos de sección con texto blanco.
|
|
748
903
|
|
|
749
904
|
### 15.2 Mapeo de tokens
|
|
@@ -755,21 +910,26 @@ El sistema es **dark-first**, pero todo producto debe soportar light mode con lo
|
|
|
755
910
|
| `--surface-2` | `#14171f` | `#ffffff` |
|
|
756
911
|
| `--surface-3` | `#1e222d` | `#eaf1f7` |
|
|
757
912
|
| `--border` | `#3d4354` | `#d8dfe8` |
|
|
758
|
-
| `--border-strong` | `#
|
|
913
|
+
| `--border-strong` | `#6b7183` (`night-400`) | `#6b7183` (`night-400`, mismo valor) |
|
|
759
914
|
| `--text-primary` | `#e8eaef` | `#14171f` |
|
|
760
915
|
| `--text-secondary` | `#c5c9d4` | `#3d4354` |
|
|
761
|
-
| `--text-muted` | `#9aa0b0` | `#
|
|
762
|
-
| `--link` / texto de acción | `#52baed` | `#
|
|
763
|
-
| `--link-hover` | `#a8ddf6` | `#
|
|
916
|
+
| `--text-muted` | `#9aa0b0` | `#5f6575` (`night-450`) |
|
|
917
|
+
| `--link` / texto de acción | `#52baed` | `#186a94` (`accent-700`) |
|
|
918
|
+
| `--link-hover` | `#a8ddf6` | `#12506f` (`accent-800`) |
|
|
764
919
|
| CTA primario (fondo/texto) | `#52baed` / `#010203` | `#52baed` / `#010203` (sin cambio) |
|
|
765
920
|
| Secundario índigo | `#353d7c` fondo | `#353d7c` fondo, texto blanco |
|
|
766
|
-
| `--success` | `#4ade80` | `#
|
|
767
|
-
| `--warning` | `#fbbf24` | `#
|
|
768
|
-
| `--danger` | `#f87171` | `#
|
|
921
|
+
| `--success` | `#4ade80` | `#166534` |
|
|
922
|
+
| `--warning` | `#fbbf24` | `#854d0e` |
|
|
923
|
+
| `--danger` | `#f87171` | `#b02c2c` |
|
|
924
|
+
| `--info` | `#52baed` | `#186a94` |
|
|
769
925
|
| `--ring` | `rgba(82,186,237,.2)` | `rgba(31,135,187,.25)` |
|
|
770
926
|
|
|
771
927
|
Fondos suaves semánticos en light: `success #e3f5ea` · `warning #fbf3dd` · `danger #fbe7e7` · `info #e3f2fb`.
|
|
772
928
|
|
|
929
|
+
> En light, el hover del texto de acción **oscurece** (`accent-700` → `accent-800`); en dark **aclara** (`accent-400` → `accent-200`). Es la misma intención —aumentar el contraste contra el fondo— aplicada a cada modo.
|
|
930
|
+
>
|
|
931
|
+
> Los semánticos de light son a la vez relleno y **texto sobre su propio `--*-bg` tintado**, así que se calibran contra ese fondo (no contra blanco): sobre blanco sobraría margen, pero sobre el fondo del badge se quedaban cortos. `--text-muted` no es `night-400` (`#6b7183`) porque ese caía a 4.27:1 sobre `--surface-3`.
|
|
932
|
+
|
|
773
933
|
### 15.3 Elevación en light
|
|
774
934
|
|
|
775
935
|
Vuelven las sombras clásicas (teñidas de índigo, no negro puro); las superficies dejan de aclararse por nivel:
|
|
@@ -786,12 +946,14 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
|
|
|
786
946
|
### 15.4 Ajustes de componentes en light
|
|
787
947
|
|
|
788
948
|
- **Botón primario:** idéntico al dark (fondo `#52baed`, texto `#010203`) — consistencia de marca entre modos.
|
|
789
|
-
- **Botón secundario:** fondo `#353d7c`, texto blanco (
|
|
790
|
-
- **Outline/Ghost:** borde `#b9c3d1`, texto `#14171f`; ghost con texto `#
|
|
791
|
-
- **Inputs:** fondo blanco, borde `#d8dfe8`; focus borde `#1f87bb` + ring.
|
|
792
|
-
- **Tabla:** header `#eaf1f7`, hover de fila `#f5f8fb`, seleccionada `rgba(82,186,237,.12)` + borde izquierdo `#1f87bb
|
|
949
|
+
- **Botón secundario:** fondo `#353d7c`, texto blanco (9.95:1 ✅).
|
|
950
|
+
- **Outline/Ghost:** borde `#b9c3d1`, texto `#14171f`; ghost con texto `#186a94` (`accent-700`, 5.95:1) y hover `rgba(31,135,187,.08)`. El texto del ghost **no** es `#1f87bb`: como texto se queda en 4.01:1.
|
|
951
|
+
- **Inputs:** fondo blanco, borde `#d8dfe8`; focus borde `#1f87bb` + ring (es UI, 4.01:1 ≥ 3 ✅).
|
|
952
|
+
- **Tabla:** header `#eaf1f7`, hover de fila `#f5f8fb`, seleccionada `rgba(82,186,237,.12)` + borde izquierdo `#1f87bb` (borde = UI ✅).
|
|
793
953
|
- **Navbar:** blanco 80% + blur, borde inferior `#d8dfe8`; link activo `#186a94`.
|
|
794
|
-
- **Sidebar activo:** fondo `#e3f2fb`, texto `#14171f`, icono `#1f87bb
|
|
954
|
+
- **Sidebar activo:** fondo `#e3f2fb`, texto `#14171f`, icono `#1f87bb` (icono = gráfico, mínimo 3:1 ✅).
|
|
955
|
+
- **Cifras y `.progress__value`:** `#186a94` — son **texto**, no relleno; con `#1f87bb` se quedaban en 4.01:1.
|
|
956
|
+
- **Badges de estado:** texto y punto en el semántico correspondiente sobre su `--*-bg`; ver §15.5.
|
|
795
957
|
- **Tooltip:** fondo `#14171f`, texto `#f6f7f9` (se invierte).
|
|
796
958
|
- **Toast:** fondo blanco, borde `#d8dfe8`, borde izquierdo semántico.
|
|
797
959
|
- **Overlay de modal:** `rgba(20,23,31,.45)` + blur 4px.
|
|
@@ -800,10 +962,36 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
|
|
|
800
962
|
|
|
801
963
|
### 15.5 Contraste en light
|
|
802
964
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
965
|
+
Ratios calculados con la fórmula WCAG 2.x, no estimados a ojo. `mcp/test/contraste-tokens.test.ts` los recalcula desde `tokens.css` en cada corrida.
|
|
966
|
+
|
|
967
|
+
**Texto (mínimo 4.5:1):**
|
|
968
|
+
|
|
969
|
+
| Color | Sobre | Ratio | |
|
|
970
|
+
|---|---|---|---|
|
|
971
|
+
| `#14171f` (`--text-primary`) | `#f5f8fb` | **16.81:1** | ✅ AAA |
|
|
972
|
+
| `#3d4354` (`--text-secondary`) | `#f5f8fb` | **9.25:1** | ✅ AAA |
|
|
973
|
+
| `#5f6575` (`night-450`, `--text-muted`) | `#eaf1f7` (`--surface-3`) | **5.11:1** | ✅ AA |
|
|
974
|
+
| `#186a94` (`accent-700`, `--link`) | blanco | **5.95:1** | ✅ AA |
|
|
975
|
+
| `#186a94` | `#f5f8fb` (`--bg`) | **5.58:1** | ✅ AA |
|
|
976
|
+
| `#12506f` (`accent-800`, `--link-hover`) | blanco | **8.74:1** | ✅ AAA |
|
|
977
|
+
| Blanco | `#353d7c` (índigo) | **9.95:1** | ✅ AAA |
|
|
978
|
+
| `#010203` | `#52baed` (CTA) | **9.50:1** | ✅ AAA |
|
|
979
|
+
|
|
980
|
+
**Semánticos — sobre su propio fondo tintado, que es donde de verdad se leen:**
|
|
981
|
+
|
|
982
|
+
| Token | Color | Sobre | Ratio | |
|
|
983
|
+
|---|---|---|---|---|
|
|
984
|
+
| `--success` | `#166534` | `#e3f5ea` | **6.29:1** | ✅ AA |
|
|
985
|
+
| `--warning` | `#854d0e` | `#fbf3dd` | **6.19:1** | ✅ AA |
|
|
986
|
+
| `--danger` | `#b02c2c` | `#fbe7e7` | **5.45:1** | ✅ AA |
|
|
987
|
+
| `--info` | `#186a94` | `#e3f2fb` | **5.20:1** | ✅ AA |
|
|
988
|
+
|
|
989
|
+
**No aptos como texto en claro:**
|
|
990
|
+
|
|
991
|
+
- `#52baed` (`accent-400`) sobre blanco: **2.19:1** ❌ — solo relleno decorativo o fondo de CTA con texto oscuro.
|
|
992
|
+
- `#1f87bb` (`accent-600`) sobre blanco: **4.01:1** ❌ como texto (AA pide 4.5) — ✅ como **componente UI** (AA pide 3): focus ring, bordes de input, borde de fila seleccionada, iconos. Ahí se queda, y conserva la firma de marca.
|
|
993
|
+
|
|
994
|
+
> **Corrección (2026-07-14).** Este bloque afirmaba que `#1f87bb` sobre blanco daba 4.5:1. Da **4.01:1**: el número se estimó y nunca se calculó. Como `--link` y `--info` salían de ahí, el texto de acción y el badge de estado nacían fallando AA — un badge sobre `--info-bg` caía a **3.50:1**. Corregido bajando el texto de acción a `accent-700`, añadiendo `accent-800` para su hover y recalibrando los cuatro semánticos contra su fondo tintado. Amarrado con test para que no vuelva a pasar.
|
|
807
995
|
|
|
808
996
|
### 15.6 Implementación
|
|
809
997
|
|
|
@@ -814,16 +1002,18 @@ El glow de acento se mantiene como firma, pero más tenue: `0 0 0 1px rgba(82,18
|
|
|
814
1002
|
--surface-2: #ffffff;
|
|
815
1003
|
--surface-3: #eaf1f7;
|
|
816
1004
|
--border: #d8dfe8;
|
|
817
|
-
--border-strong:
|
|
1005
|
+
/* --border-strong no se redefine: night-400 (:root) cumple 1.4.11 en los dos temas */
|
|
818
1006
|
--text-primary: #14171f;
|
|
819
1007
|
--text-secondary: #3d4354;
|
|
820
|
-
--text-muted: #
|
|
821
|
-
--link: #
|
|
822
|
-
--link-hover: #
|
|
1008
|
+
--text-muted: #5f6575; /* night-450 — night-400 caía a 4.27:1 sobre --surface-3 */
|
|
1009
|
+
--link: #186a94; /* accent-700 · 5.95:1 — accent-600 solo da 4.01:1 */
|
|
1010
|
+
--link-hover: #12506f; /* accent-800 · en light el hover oscurece */
|
|
823
1011
|
--text-on-accent: #010203; /* CTA no cambia */
|
|
824
|
-
|
|
825
|
-
--
|
|
826
|
-
--
|
|
1012
|
+
/* Semánticos: son texto sobre su propio --*-bg tintado, no sobre blanco. */
|
|
1013
|
+
--success: #166534;
|
|
1014
|
+
--warning: #854d0e;
|
|
1015
|
+
--danger: #b02c2c;
|
|
1016
|
+
--info: #186a94;
|
|
827
1017
|
--ring: 0 0 0 3px rgba(31,135,187,.25);
|
|
828
1018
|
--shadow-1: 0 1px 3px rgba(53,61,124,.08);
|
|
829
1019
|
--shadow-2: 0 4px 10px rgba(53,61,124,.10);
|