@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
|
@@ -306,13 +306,14 @@ Export propio (extraído del estado loading de Button). SVG 16px default: círcu
|
|
|
306
306
|
- **Disabled:** fondo `neutral-100`, texto `--text-disabled`, cursor `not-allowed`.
|
|
307
307
|
- **Texto de ayuda:** `text-sm` `--text-muted` debajo del input.
|
|
308
308
|
- Con icono leading: padding-left 40px, icono 18px `--text-disabled`.
|
|
309
|
-
- **`inputmode` según el dato** (`numeric`, `email`, `tel`, `decimal`) para el teclado correcto en móvil; `spellcheck="false"` en emails, códigos y usernames.
|
|
309
|
+
- **`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.
|
|
310
310
|
- **Nunca bloquear el pegado** (`onpaste`): rompe gestores de contraseñas y códigos de verificación.
|
|
311
311
|
- **Al enviar con errores,** mover el foco al primer campo inválido (no solo pintarlo de rojo).
|
|
312
|
+
- **Campo obligatorio (`.field--required`):** el `.field__label` muestra un asterisco decorativo vía `::after` en color `--danger-500`, y el `<input>` lleva `required` + `aria-required="true"` como señal accesible real (el asterisco es cromo, no el mecanismo). No marques campos "opcionales" con asterisco: el asterisco significa obligatorio.
|
|
312
313
|
|
|
313
314
|
### 10.5 Textarea
|
|
314
315
|
|
|
315
|
-
Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px. Contador de caracteres `text-xs` `--text-
|
|
316
|
+
Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px. Contador de caracteres (`.textarea-counter`) `text-xs` `--text-muted` alineado a la derecha.
|
|
316
317
|
|
|
317
318
|
### 10.6 Select
|
|
318
319
|
|
|
@@ -326,7 +327,7 @@ Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px
|
|
|
326
327
|
Pill compacta de **2–4 opciones mutuamente excluyentes** (switches de vista, filtros rápidos). Para >4 opciones o labels largos, usar `Select`.
|
|
327
328
|
|
|
328
329
|
- Contenedor: fondo `--surface-alt`, radius `md`, padding 3px. Altura 40px (md) / 32px (sm); segmentos = altura − 6px, padding horizontal 16px.
|
|
329
|
-
-
|
|
330
|
+
- Opción activa: fondo blanco (`--surface`), radius `calc(radius-md - 3px)`, `shadow-xs`; **transición de fondo y color 200ms `--ease-out`** (swap de color, nunca animando `left`/`width`).
|
|
330
331
|
- Segmento activo: `--text-primary` semibold; inactivo: `--text-secondary` medium; transición de color 150ms. Texto `text-sm`.
|
|
331
332
|
|
|
332
333
|
### 10.8 Slider
|
|
@@ -344,6 +345,7 @@ Rango de valor único.
|
|
|
344
345
|
- Checked: fondo `--accent`, check blanco animado (stroke-dashoffset, 200ms).
|
|
345
346
|
- Hover: borde `primary-400`. Focus: ring como inputs.
|
|
346
347
|
- Label clicable, gap 10px, `text-base`. `RadioGroup` agrupa con `role="radiogroup"`.
|
|
348
|
+
- **Indeterminate (`input:indeterminate`):** fondo `--accent` con un guion horizontal (10×2px, `--text-on-primary`) en vez del check. Es el estado de un checkbox "seleccionar todo" cuando la selección es parcial. Es una **propiedad del DOM** (`checkbox.indeterminate = true`), no un atributo HTML: se fija por JS y se sincroniza con las filas seleccionadas (ver §10.21).
|
|
347
349
|
|
|
348
350
|
### 10.10 Switch
|
|
349
351
|
|
|
@@ -385,7 +387,7 @@ Banner de estado **inline y persistente** (vs Toast, transitorio): errores de fo
|
|
|
385
387
|
|
|
386
388
|
- 4 tonos: fondo suave semántico (`*-100`), radius `md`, padding `14px 16px`.
|
|
387
389
|
- Icono semántico 20px en `*-500`: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`.
|
|
388
|
-
- Título `text-sm` semibold y cuerpo `text-sm` en `--*-text
|
|
390
|
+
- Título `text-sm` semibold y cuerpo `text-sm` en `--*-text`, a contraste completo (sin opacidad).
|
|
389
391
|
- Botón cerrar opcional (× 16px en el color del tono) vía `onDismiss`. `role="status"`.
|
|
390
392
|
|
|
391
393
|
### 10.17 Toast
|
|
@@ -409,9 +411,17 @@ Progreso **determinado** (para carga indeterminada usar `Skeleton`, no Progress)
|
|
|
409
411
|
- Panel: fondo blanco, radius `lg`, `shadow-xl`, max-width 480px (confirm) / 640px (formulario), padding 24px.
|
|
410
412
|
- Entrada: fade + scale(0.96→1), 250ms. Salida: 180ms.
|
|
411
413
|
- Header con título `text-xl` + IconButton cerrar; footer con acciones alineadas a la derecha (primaria al extremo).
|
|
412
|
-
- Cerrar con Esc y click en overlay (excepto flujos destructivos).
|
|
414
|
+
- Cerrar con Esc y click en overlay (excepto flujos destructivos: el overlay no cierra para evitar descartes accidentales; Esc y los botones sí).
|
|
413
415
|
- `overscroll-behavior: contain` en el panel scrolleable — al llegar al fondo, el scroll no se encadena a la página de atrás.
|
|
414
416
|
|
|
417
|
+
**Accesibilidad y foco (obligatorio):**
|
|
418
|
+
|
|
419
|
+
- **Roles y etiquetado:** el contenedor lleva `role="dialog"`, `aria-modal="true"` y `aria-labelledby` apuntando al id del título (`.modal__title`). Si hay texto descriptivo, `aria-describedby` con el id de ese párrafo.
|
|
420
|
+
- **Foco al abrir:** el foco entra al panel (`.modal__panel` con `tabindex="-1"`) o al primer control enfocable del modal.
|
|
421
|
+
- **Focus-trap:** mientras está abierto, Tab y Shift+Tab ciclan solo entre los elementos enfocables del modal; el foco nunca escapa al contenido de atrás (que además está inerte por el overlay).
|
|
422
|
+
- **Retorno de foco:** al cerrar (Esc, botón ×, Cancelar o click en overlay), el foco vuelve al elemento que abrió el modal.
|
|
423
|
+
- El panel se marca con `tabindex="-1"` para poder recibir foco programático sin quedar en el orden de tabulación.
|
|
424
|
+
|
|
415
425
|
### 10.20 Menu
|
|
416
426
|
|
|
417
427
|
Lista de acciones desplegable anclada a un trigger (acciones de fila, botones "más", menú de cuenta).
|
|
@@ -424,11 +434,20 @@ Lista de acciones desplegable anclada a un trigger (acciones de fila, botones "m
|
|
|
424
434
|
|
|
425
435
|
### 10.21 Tabla
|
|
426
436
|
|
|
427
|
-
- Header: fondo `neutral-50`, `text-xs` uppercase `--tracking-label`, `--text-muted`, borde inferior `--border`.
|
|
437
|
+
- Header: fondo `neutral-50`, `text-xs` uppercase `--tracking-label`, `--text-muted`, borde inferior `--border`. Cada `<th>` lleva `scope="col"`.
|
|
428
438
|
- Filas: altura 52px, borde inferior `neutral-100`, hover fondo `neutral-50`.
|
|
429
439
|
- Celdas numéricas alineadas a la derecha con `tabular-nums`.
|
|
430
440
|
- Fila seleccionada: fondo `--accent-soft`.
|
|
431
441
|
|
|
442
|
+
**Variantes (clases del framework):**
|
|
443
|
+
|
|
444
|
+
- **`.table--zebra`** — filas alternas (`tr:nth-child(even)`) con fondo `--surface-alt`; el hover sigue ganando.
|
|
445
|
+
- **`.table--sticky`** — `thead th` con `position: sticky; top: 0` (fondo opaco del header + `z-index`). El contenedor scrolleable acota la altura para que el header quede fijo.
|
|
446
|
+
- **Header ordenable** — dentro del `<th>` un `<button class="th-sort">` (o `.th-sort--num` para columnas numéricas) con el texto + `.sort-icon`. El `<th>` declara `aria-sort="none|ascending|descending"`; el JS alterna el valor y reordena el `tbody`. El icono se atenúa en `none` y toma `--accent` (rotado 180° en `descending`) al ordenar.
|
|
447
|
+
- **Columna de selección** — `.col-select` (44px, centrada) con un `.checkbox` por fila (`aria-label` describiendo la fila) y un checkbox "seleccionar todo" en el `<th>`. La fila marcada lleva `.row--selected` (fondo `--accent-soft`); el checkbox del header pasa a `indeterminate` cuando la selección es parcial (ver §10.9).
|
|
448
|
+
|
|
449
|
+
**Accesibilidad:** `scope="col"` en headers; `aria-sort` en la columna ordenable; el `.sort-icon` es `aria-hidden` (el estado real lo da `aria-sort` + el texto del header); cada checkbox de fila con `aria-label` propio.
|
|
450
|
+
|
|
432
451
|
### 10.22 Stat
|
|
433
452
|
|
|
434
453
|
Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
|
|
@@ -437,6 +456,26 @@ Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
|
|
|
437
456
|
- Valor grande en `--font-mono text-3xl` semibold `--text-primary` (tracking -0.01em, line-height 1).
|
|
438
457
|
- Delta opcional en baseline (gap 10px): `text-sm` semibold con flecha 14px (`trending-up`/`trending-down`/`minus`). Tono derivado del signo (− → danger, resto success) u override `deltaTone`; colores del delta en `--*-text` (no los `-500`).
|
|
439
458
|
|
|
459
|
+
#### Chip de métrica
|
|
460
|
+
|
|
461
|
+
Pastilla de una línea con rótulo y valor —por ejemplo, «Agua **3 de 8**»— para métricas secundarias que no merecen un Stat completo.
|
|
462
|
+
|
|
463
|
+
- **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
|
|
464
|
+
- **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va semibold en `--link` con `tabular-nums`.
|
|
465
|
+
- **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.
|
|
466
|
+
- **Interactivo** (por ejemplo, «sumar un vaso»): `button.metric-chip`, con `min-height: var(--control-height-md)` (44px) y padding horizontal de 16px. Hover: fondo `--accent-soft` y borde `--accent`. Active: fondo `--accent-soft-strong` y `scale(.98)`. Foco de teclado con el anillo del sistema (outline 2px `--accent`, offset 2px).
|
|
467
|
+
|
|
468
|
+
```html
|
|
469
|
+
<div class="metric-chips">
|
|
470
|
+
<span class="metric-chip"><span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span></span>
|
|
471
|
+
<button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
|
|
472
|
+
<span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**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.
|
|
478
|
+
|
|
440
479
|
### 10.23 Pagination
|
|
441
480
|
|
|
442
481
|
Navegador de páginas 1-indexado, bajo una `Table`.
|
|
@@ -453,6 +492,37 @@ Navegador de páginas 1-indexado, bajo una `Table`.
|
|
|
453
492
|
- **Tabs:** borde inferior `--border`; tab activa con indicador 2px `--accent` que se desliza entre tabs (250ms).
|
|
454
493
|
- **Breadcrumbs:** `text-sm` `--text-muted`, separador `/` (tipográfico), último ítem `--text-primary`.
|
|
455
494
|
|
|
495
|
+
#### Sidebar contraíble
|
|
496
|
+
|
|
497
|
+
Variante del Sidebar para apps con muchas secciones: alterna entre la lista con rótulos y un riel de iconos que deja más ancho al contenido.
|
|
498
|
+
|
|
499
|
+
- **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`.
|
|
500
|
+
- **Grupos con rótulo:** `.sidebar__group` con un `.sidebar__group-label` (`text-xs` semibold en mayúsculas, tracking `--tracking-label`, `--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.
|
|
501
|
+
- **Ítem activo:** `.is-active` (fondo `--accent-soft-strong`, texto `--link`) más `aria-current="page"`.
|
|
502
|
+
- **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`.
|
|
503
|
+
- **Tooltip en el riel:** el `.sidebar__label` queda oculto a la vista pero accesible (patrón de `.u-visually-hidden`) y aparece a la derecha del ítem como tooltip (`--tooltip-bg`, `text-xs`, radius `sm`, 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.
|
|
504
|
+
- **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).
|
|
505
|
+
- **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 (reset global, §11.5).
|
|
506
|
+
|
|
507
|
+
```html
|
|
508
|
+
<nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
|
|
509
|
+
<div class="sidebar__group" role="group" aria-labelledby="navHoy">
|
|
510
|
+
<span class="sidebar__group-label" id="navHoy">Hoy</span>
|
|
511
|
+
<a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
|
|
512
|
+
</div>
|
|
513
|
+
<button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
|
|
514
|
+
<svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
|
|
515
|
+
</button>
|
|
516
|
+
</nav>
|
|
517
|
+
|
|
518
|
+
<!-- Solo por debajo de 768px -->
|
|
519
|
+
<button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
|
|
520
|
+
aria-expanded="false" aria-controls="navDrawer">…menu…</button>
|
|
521
|
+
<div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
**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.
|
|
525
|
+
|
|
456
526
|
### 10.25 Skeleton y EmptyState
|
|
457
527
|
|
|
458
528
|
- Skeleton: fondo `--surface-alt` con shimmer (gradiente que barre en 1.4s, loop). Radius igual al contenido que reemplaza.
|
|
@@ -462,9 +532,100 @@ Navegador de páginas 1-indexado, bajo una `Table`.
|
|
|
462
532
|
|
|
463
533
|
- **Card:** fondo `--surface`, borde `1px --border`, radius `lg`, padding 24px (16px en móvil), `shadow-sm`.
|
|
464
534
|
- **Card interactiva:** cursor pointer; hover eleva — `translateY(-2px)` + `shadow-md`; transición de transform/sombra 200ms (`--duration-base`) ease-out. Sin glow: en light-first la elevación es sombra + desplazamiento, nunca brillo.
|
|
465
|
-
- **Link:** color `--link` (primary-700, AA
|
|
535
|
+
- **Link:** color `--link` (primary-700, AA 5.94:1); subrayado animado 1px `currentColor` que crece de 0%→100% del ancho desde la izquierda (`background-size`, 200ms ease-out). Externo: `target="_blank" rel="noopener noreferrer"` + icono flecha 12px, gap 4px.
|
|
466
536
|
- **Code (inline):** IBM Plex Mono `0.9em`, texto `--text-primary`, fondo `--surface-alt`, borde `1px --border`, radius `sm`, padding `1px 6px`.
|
|
467
537
|
|
|
538
|
+
### 10.27 AvatarGroup
|
|
539
|
+
|
|
540
|
+
Stack de avatares solapados para representar participantes de un proyecto, miembros de un equipo o asignados de una tarea. Complementa al `Avatar` (§10.12).
|
|
541
|
+
|
|
542
|
+
- **`.avatar-group`** — contenedor `inline-flex`; cada `.avatar` (y el `.avatar-group__more`) se solapa con `margin-left: -8px`.
|
|
543
|
+
- **Anillo de separación** — cada avatar del grupo lleva un borde `2px solid var(--surface)` (radius full) que lo recorta del de atrás; en dark hereda el `--surface` oscuro. El orden de pintado deja el último encima.
|
|
544
|
+
- **`.avatar-group__more`** — chip circular del mismo tamaño (36px) para el `+N` de overflow: fondo `--accent-soft-strong`, texto `--text-on-primary-solid` semibold `text-xs`, mismo anillo y solapamiento. Va al final del grupo.
|
|
545
|
+
- Para más de ~5 avatares, corta la lista y muestra el resto como `+N` (evita filas larguísimas).
|
|
546
|
+
|
|
547
|
+
### 10.28 Combobox / Autocomplete y Búsqueda
|
|
548
|
+
|
|
549
|
+
Campo de texto con lista de opciones filtrable (Combobox) y su primo especializado para búsqueda (Search). Para una lista fija sin filtrado, usar `Select` (§10.6); para texto libre sin sugerencias, `Input` (§10.4).
|
|
550
|
+
|
|
551
|
+
**Combobox (`.combobox`):**
|
|
552
|
+
|
|
553
|
+
- **`.combobox__input`** — input `role="combobox"` con `aria-expanded`, `aria-controls` (id de la lista), `aria-autocomplete="list"` y `aria-activedescendant` apuntando a la opción activa. Mismo look que `Input` (borde `--border-strong`, focus `--accent` + `--ring`).
|
|
554
|
+
- **`.combobox__list`** — dropdown `role="listbox"` con id; fondo `--surface`, `shadow-lg`, radius `md`, max-height 264px con scroll. Entra con fade + translateY(-4px→0), `--duration-base` `--ease-out`.
|
|
555
|
+
- **`.combobox__option`** — `role="option"` + `aria-selected`. `.is-active` = resaltado de teclado (fondo `--accent-soft`); `[aria-selected="true"]` = elegida (texto `--link`, medium, check `--accent` a la derecha — el estado no se comunica solo por color).
|
|
556
|
+
- **Teclado:** ↑/↓ mueven la opción activa **sin mover el foco** (que se queda en el input); Enter selecciona, Esc cierra, Home/End saltan a los extremos. La opción activa se señala con `aria-activedescendant`, no con foco real.
|
|
557
|
+
|
|
558
|
+
**Búsqueda (`.search`):**
|
|
559
|
+
|
|
560
|
+
- **`.search__icon`** — lupa 18px a la izquierda, `aria-hidden` (decorativa).
|
|
561
|
+
- **`.search__input`** — input con padding lateral 40px para dejar sitio al icono y a la ×.
|
|
562
|
+
- **`.search__clear`** — botón × que aparece solo cuando hay texto (`:not(:placeholder-shown)`), `aria-label="Limpiar"`; al pulsarlo vacía el campo y le devuelve el foco.
|
|
563
|
+
|
|
564
|
+
### 10.29 Drawer / Sheet
|
|
565
|
+
|
|
566
|
+
Panel que entra deslizando desde un borde para tareas contextuales (filtros, detalle, formulario lateral) sin sacar al usuario de la vista. Modal en comportamiento, lateral en forma.
|
|
567
|
+
|
|
568
|
+
- **`.drawer`** — contenedor/estado a pantalla completa (`position: fixed`), oculto hasta `.is-open`.
|
|
569
|
+
- **`.drawer__overlay`** — capa `--overlay` que atenúa el fondo; cierra al hacer click.
|
|
570
|
+
- **`.drawer__panel`** — panel `--surface` con `shadow-xl`; `overscroll-behavior: contain` para que el scroll no se encadene a la página. El contenedor lleva `role="dialog"` + `aria-modal="true"` + `aria-labelledby`.
|
|
571
|
+
- **Variantes:** `.drawer--right` (default), `.drawer--left`, `.drawer--bottom` (esta última full-width, con radius superior `lg`).
|
|
572
|
+
- **Foco (igual que Modal, §10.19):** al abrir, el foco entra al panel (`tabindex="-1"`), queda atrapado (Tab/Shift+Tab ciclan) y vuelve al disparador al cerrar. Esc y click en overlay cierran.
|
|
573
|
+
- **Motion:** entra con `translateX(100%)→0` (o `translateY` en la variante bottom) + fade del overlay, `--duration-slow` `--ease-out`; la salida es más corta (`--duration-base`, `--ease-in`). Solo `transform`/`opacity`, bajo el techo `--duration-page`.
|
|
574
|
+
|
|
575
|
+
### 10.30 Popover
|
|
576
|
+
|
|
577
|
+
Contenedor flotante con **contenido interactivo** anclado a un disparador. A diferencia del Tooltip (§10.15, solo texto y aparición en hover), el Popover se abre por **click** y puede llevar enlaces, botones o campos.
|
|
578
|
+
|
|
579
|
+
- **`.popover`** — contenedor relativo (`.is-open` lo abre).
|
|
580
|
+
- **`.popover__trigger`** — disparador con `aria-haspopup="dialog"` + `aria-expanded`.
|
|
581
|
+
- **`.popover__panel`** — `role="dialog"` + `aria-label`/`aria-labelledby`; fondo `--surface`, borde `--border`, `shadow-lg`, radius `md`. El foco entra al panel al abrir; cierra con Esc y click fuera.
|
|
582
|
+
- **Motion:** aparece desde el `transform-origin` del trigger (fade + `scale(0.96→1)`), `--duration-base` `--ease-out` — sin rebote (es un utilitario). Variante `.popover--right` ancla el panel a la derecha.
|
|
583
|
+
|
|
584
|
+
### 10.31 ButtonGroup
|
|
585
|
+
|
|
586
|
+
Botones adyacentes con bordes compartidos para acciones relacionadas (barra de fechas, alineación, cambio de vista). Para un único-seleccionado mutuamente excluyente, usar `SegmentedControl` (§10.7).
|
|
587
|
+
|
|
588
|
+
- **`.btn-group`** — contenedor `inline-flex` con `role="group"` + `aria-label`. Usa los `.btn`/`.btn--secondary` existentes dentro.
|
|
589
|
+
- **Bordes:** radius solo en los extremos (el primer hijo redondea la izquierda, el último la derecha); los bordes internos se colapsan con `margin-left: -1px` para no duplicar la línea. En hover/focus el botón sube en `z-index` para que su borde y su ring queden por encima del vecino.
|
|
590
|
+
|
|
591
|
+
### 10.32 DatePicker
|
|
592
|
+
|
|
593
|
+
Campo de fecha con un calendario en popover. Reusa el patrón visual del `Popover` (§10.30, panel `--surface` + `shadow-lg`, aparición desde el `transform-origin` del disparador) y la navegación por teclado del `Combobox` (§10.28).
|
|
594
|
+
|
|
595
|
+
- **`.datepicker`** — contenedor relativo (`.is-open` lo abre). **`.datepicker__input`** con el look de `Input` (borde `--border-strong`, focus `--accent` + `--ring`), `readonly` + `aria-haspopup="dialog"` + `aria-controls` (sin `aria-expanded`, que no es válido en un `input` de texto; el estado abierto lo da `.is-open`); un `.datepicker__icon` de calendario 18px a la derecha (decorativo, `aria-hidden`).
|
|
596
|
+
- **`.datepicker__calendar`** — panel flotante `role="dialog"` con `aria-label`, mismo patrón que `.popover__panel`. Variante `.datepicker--right` ancla a la derecha.
|
|
597
|
+
- **`.calendar`** — dentro del panel: `.calendar__header` (mes/año en `.calendar__title` + botones prev/next con `#i-chevron-left/right`), `.calendar__table` (`role="grid"`) que envuelve la fila de cabeceras `.calendar__weekdays` (`role="row"`, 7 encabezados `role="columnheader"` con `aria-label` completo: "lunes"…) y `.calendar__grid` (`role="rowgroup"`) con una `.calendar__week` (`role="row"`, `display: contents` para no romper la rejilla) por semana, y `.calendar__day` (botón `role="gridcell"`, `aria-label` de la fecha completa vía `Intl.DateTimeFormat` y `aria-selected`). Semana empieza en lunes.
|
|
598
|
+
- **Estados de día:** `.calendar__day--today` (semibold + anillo interior `--border-strong`), `.calendar__day--selected` (fondo `--accent`, texto `--text-on-primary`), `.calendar__day--outside` (días de otro mes, `--text-muted`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
|
|
599
|
+
- **Teclado y foco (como Popover):** el input abre por click (o Enter/Espacio/↓); al abrir, el foco entra al calendario (día enfocado). Dentro de la rejilla las **flechas** mueven el foco entre días (roving `tabindex`, cambiando de mes al cruzar el borde), Home/End saltan a los extremos de la semana, PageUp/PageDown cambian de mes, Enter/Espacio selecciona; **Esc** cierra y devuelve el foco al input. La fecha se formatea con `Intl` (`es-MX`), nunca a mano.
|
|
600
|
+
- **Motion:** el panel aparece con fade + `scale(0.96→1)`, `--duration-base` `--ease-out`, bajo el techo `--duration-page`. Solo `transform`/`opacity`.
|
|
601
|
+
|
|
602
|
+
### 10.33 FileUpload
|
|
603
|
+
|
|
604
|
+
Zona de arrastre (dropzone) + lista de archivos.
|
|
605
|
+
|
|
606
|
+
- **`.file-upload`** — dropzone: borde `2px dashed --border-strong`, radius `lg`, fondo `--surface`. Estado **`.file-upload.is-dragover`** (borde `--accent` + fondo `--accent-soft`) mientras se arrastra un archivo encima.
|
|
607
|
+
- **`.file-upload__input`** — el `<input type="file">` real, oculto de forma accesible (no `display:none`), con **label asociado** (`.file-upload__label for=…`) y `aria-describedby` que apunta al hint de formatos/tamaño. La zona describe la acción ("Arrastra archivos o elige").
|
|
608
|
+
- **`.file-upload__label`** — área central con icono de subida (`#i-upload`), `.file-upload__title` (instrucción), `.file-upload__button` (pseudo-botón "Elegir archivos" — no un `<button>` real, para no anidar interactivos dentro del label) y `.file-upload__hint` (formatos y tamaño máximo).
|
|
609
|
+
- **`.file-upload__list`** / **`.file-upload__item`** — la lista de subidas. Cada item: `.file-upload__icon` (`#i-file`, o `#i-circle-alert` en error), `.file-upload__name` (trunca con ellipsis), `.file-upload__size` (metadatos/estado) y `.file-upload__remove` (× 32px con `aria-label` que nombra el archivo). Un item que sube incluye la barra `.progress` (§10.18) dentro de su meta.
|
|
610
|
+
- **Error (`.file-upload__item--error`):** borde `--danger-500`, icono y texto de estado en `--danger-text`; **el estado no se comunica solo por color** — el item lleva icono de alerta + texto que explica qué pasó y qué hacer ("Formato no admitido · elige PDF, PNG o JPG").
|
|
611
|
+
- **JS mínimo:** `is-dragover` en dragenter/dragover, se quita en dragleave/drop; el × elimina su item. La subida real y su validación son responsabilidad de la app.
|
|
612
|
+
|
|
613
|
+
### 10.34 Stepper / Wizard
|
|
614
|
+
|
|
615
|
+
Indicador de progreso multipaso.
|
|
616
|
+
|
|
617
|
+
- **`.stepper`** — un **`<ol>`** horizontal; `.stepper--vertical` lo apila. Cada **`.step`** contiene `.step__marker` (círculo 32px), `.step__label` y, salvo el último, un `.step__connector` (línea de 2px hacia el siguiente paso).
|
|
618
|
+
- **Estados:** `.step--complete` (marcador `--accent` con **`#i-check`** blanco, conector `--accent`), `.step--current` (marcador con borde e ícono/número `--accent`, label semibold `--text-primary`, `aria-current="step"`), `.step--upcoming` (marcador neutro con su **número**). El completo muestra check y el próximo muestra número: **el estado no depende solo del color**.
|
|
619
|
+
- **Conector:** hereda el color de su paso — se pinta `--accent` solo tras un paso completo, marcando hasta dónde llegó el progreso; el marcador va en `z-index` superior para que el círculo tape la línea.
|
|
620
|
+
- a11y: lista ordenada real (`<ol>`), `aria-current="step"` en el actual, iconos decorativos (`aria-hidden`); el número/label textual da la señal no cromática.
|
|
621
|
+
|
|
622
|
+
### 10.35 Kbd
|
|
623
|
+
|
|
624
|
+
Tecla del teclado para atajos y ayuda contextual.
|
|
625
|
+
|
|
626
|
+
- **`.kbd`** (para `<kbd>`) — tecla con borde `--border-strong`, radius `sm`, **sombra sutil de tinta** (`--shadow-xs`), tipografía `--font-mono` `text-xs`, color `--text-secondary`, **mínimo 20px** de ancho/alto. Funciona inline en el texto o suelta.
|
|
627
|
+
- **Combos:** varias `.kbd` con el signo **"+" literal** entre teclas (envueltas en `.kbd-combo` para alinear el "+" en `--text-muted`). Ejemplos: `⌘ + K`, `Ctrl + S`, `Esc`.
|
|
628
|
+
|
|
468
629
|
---
|
|
469
630
|
|
|
470
631
|
## 11. Animaciones y motion
|
|
@@ -509,7 +670,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
|
|
|
509
670
|
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.
|
|
510
671
|
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).
|
|
511
672
|
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.
|
|
512
|
-
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
|
|
673
|
+
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`.
|
|
513
674
|
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.
|
|
514
675
|
|
|
515
676
|
### 11.3 Micro-interacciones por componente
|
|
@@ -523,7 +684,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
|
|
|
523
684
|
| Input | focus | ring que aparece con fade 150ms |
|
|
524
685
|
| Checkbox | check | trazo del check dibujándose 200ms `ease-spring` |
|
|
525
686
|
| Toggle | cambio | thumb desliza 200ms `ease-spring` |
|
|
526
|
-
| SegmentedControl | cambio |
|
|
687
|
+
| SegmentedControl | cambio | la opción activa toma fondo blanco 200ms `--ease-out` |
|
|
527
688
|
| Slider | press | thumb escala 1.15, 100ms |
|
|
528
689
|
| Acordeón | abrir | `grid-template-rows` (0fr→1fr) 250ms + fade del contenido |
|
|
529
690
|
| Dropdown / Menu | abrir | fade + translateY(-4px→0) 160–180ms |
|
|
@@ -601,11 +762,13 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
|
|
|
601
762
|
|
|
602
763
|
## 12. Accesibilidad y contraste
|
|
603
764
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
-
|
|
765
|
+
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.
|
|
766
|
+
|
|
767
|
+
- `neutral-800` sobre blanco: **12.42:1** ✅ AAA.
|
|
768
|
+
- Blanco sobre `primary-500`: **3.22:1** ⚠️ solo texto ≥18px/bold o componentes UI; para texto normal usar fondo `primary-700`.
|
|
769
|
+
- `--link` (`primary-700`) sobre blanco: **5.94:1** ✅ AA — por eso links y ghost usan `--link`, no `primary-500`.
|
|
607
770
|
- `primary-300` (#a0c0d4) **nunca como texto sobre blanco** — solo decorativo.
|
|
608
|
-
- Tonos `--*-text` garantizan AA sobre los fondos suaves `*-100
|
|
771
|
+
- Tonos `--*-text` garantizan AA sobre los fondos suaves `*-100`: success **5.30:1** · warning **4.72:1** · danger **6.60:1** · info **4.83:1**.
|
|
609
772
|
- Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y UI.
|
|
610
773
|
- Focus visible siempre (ring global `:focus-visible`, §6); estado nunca comunicado solo con color (icono + texto).
|
|
611
774
|
- Touch targets mínimos: 44×44px.
|
|
@@ -772,7 +935,7 @@ Fondos suaves semánticos en dark: `success #12291f` · `warning #2b2210` · `da
|
|
|
772
935
|
|
|
773
936
|
### 15.4 Ajustes de componentes en dark
|
|
774
937
|
|
|
775
|
-
- **Botón primario:** fondo `#87a8c7`, texto `#16181e` (
|
|
938
|
+
- **Botón primario:** fondo `#87a8c7`, texto `#16181e` (7.14:1 ✅). Hover `#a0c0d4`.
|
|
776
939
|
- **Inputs:** fondo `#252935`, borde `#3a3f4d`, placeholder `#8b91a0`; focus borde `#a0c0d4` + ring.
|
|
777
940
|
- **Tabla:** header `#1d2028`, hover de fila `#252935`, seleccionada `rgba(160,192,212,.1)`.
|
|
778
941
|
- **Sidebar activo:** fondo `rgba(113,146,186,.15)`, texto `#e6e9ee`, icono `#a0c0d4`.
|
|
@@ -783,9 +946,12 @@ Fondos suaves semánticos en dark: `success #12291f` · `warning #2b2210` · `da
|
|
|
783
946
|
|
|
784
947
|
### 15.5 Contraste en dark
|
|
785
948
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
- `#
|
|
949
|
+
Ratios calculados, no estimados (mismo test que §12).
|
|
950
|
+
|
|
951
|
+
- `#e6e9ee` sobre `#16181e`: **14.58:1** ✅ AAA.
|
|
952
|
+
- `#a0c0d4` sobre `#16181e`: **9.28:1** ✅ — en dark el celeste **sí** puede ser texto/link (al revés que en light).
|
|
953
|
+
- `#7192ba` sobre `#16181e`: **5.51:1** ✅ AA — usable en texto normal.
|
|
954
|
+
- Tonos `--*-text` sobre sus `*-100` en dark: success **8.76:1** · warning **9.60:1** · danger **8.03:1** · info **8.20:1**.
|
|
789
955
|
|
|
790
956
|
### 15.6 Implementación
|
|
791
957
|
|