@higinio/design-mcp 0.1.1 → 0.2.1

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.
@@ -96,7 +96,7 @@ Los componentes **nunca** usan los primitivos directamente: consumen esta capa.
96
96
  | `--text-primary` | `neutral-800` | Texto principal |
97
97
  | `--text-secondary` | `neutral-600` | Texto secundario |
98
98
  | `--text-muted` | `neutral-500` | Terciario, captions |
99
- | `--text-disabled` | `neutral-400` | Deshabilitado, placeholder |
99
+ | `--text-disabled` | `neutral-400` | Solo controles deshabilitados |
100
100
  | `--text-on-primary` | `#ffffff` | Texto sobre botón primario |
101
101
  | `--text-on-primary-solid` | `primary-700` | Texto de peso normal sobre `primary-100` |
102
102
  | `--accent` | `primary-500` | Rellenos de acción, bordes activos, focus |
@@ -109,26 +109,32 @@ Los componentes **nunca** usan los primitivos directamente: consumen esta capa.
109
109
 
110
110
  > ⚠️ **Regla AA de acento:** `primary-500` sobre blanco da ~3:1 — válido solo para **componentes UI** (rellenos, bordes, focus). Para **texto** (links, ghost) usar `--link` (`primary-700`, ~6:1). `primary-300` jamás como texto.
111
111
 
112
- ### 3.5 Proporción de uso (regla 60-30-10)
112
+ ### 3.5 Uso del color y jerarquía
113
113
 
114
- - **60% — Neutrales claros** (`neutral-0/50/100`): fondos y superficies.
115
- - **30% — Tinta y estructura** (`neutral-600→900`, bordes): texto, iconos, divisores.
116
- - **10% — Primario y semánticos**: botones, links, estados, gráficas. **El acento pierde poder si supera el 10%.** (Excepción: una visualización de datos multiserie usa la paleta categórica validada de la skill `dataviz-marca`, exenta de este techo — son datos, no cromo decorativo.)
114
+ El primitivo de marca expresa identidad; el rol de acción asegura legibilidad. Una acción dominante por contexto de decisión: un formulario, un diálogo o una región independiente pueden tener su propia acción. Evita competir dentro del mismo grupo. La proporción de acento se decide por jerarquía y contenido; 60/30/10 es una orientación visual, no una cuota ni un criterio de conformidad.
117
115
 
118
- Reglas prácticas:
119
- - **Un solo botón primario visible por pantalla**; el resto secundarios o ghost.
120
- - **Fondos siempre planos** (`--bg`): sin gradientes, texturas ni patrones; sin full-bleed decorativo — la superficie de trabajo (tarjetas blancas) es la protagonista.
121
- - Estado nunca solo con color: siempre color + icono + texto.
116
+ | Rol semántico | Uso |
117
+ |---|---|
118
+ | `--action-bg`, `--action-hover`, `--action-active` | Relleno de acciones sólidas y selección con texto |
119
+ | `--text-on-action` | Texto e iconos sobre los tres fondos de acción; ≥4.5:1 |
120
+ | `--control-border` | Contorno necesario para reconocer campos, checkbox y radio; ≥3:1 |
121
+ | `--focus-color` | Contorno sólido de teclado, distinto del halo decorativo |
122
+ | `--placeholder` | Ayuda legible dentro de campos; nunca sustituye al label |
123
+ | `--selection-bg`, `--selection-text` | Selección nativa de texto, con contraste propio |
124
+
125
+ `--accent` conserva la firma cromática y no garantiza contraste como relleno de texto normal. No lo intercambies con `--action-bg`. Los separadores decorativos usan `--border`; los controles usan `--control-border`.
126
+
127
+ Para datos categóricos consulta `.claude/skills/dataviz-marca/references/paletas-graficas.md`: allí se declara la validación de cada paleta, sus límites y los fallbacks. La identidad de serie también necesita etiqueta, forma o patrón; un cambio de tendencia no significa éxito por defecto.
122
128
 
123
129
  ---
124
130
 
125
131
  ## 4. Tipografía
126
132
 
127
- ### 4.1 Familias (roles estrictos)
133
+ ### 4.1 Familias y funciones
128
134
 
129
135
  | Rol | Fuente | Fallback | Uso |
130
136
  |---|---|---|---|
131
- | Display | **Fraunces** (600–700) | Georgia, serif | **Solo h1–h2**, cifras hero, títulos de marketing — nunca UI ni párrafos |
137
+ | Display | **Fraunces** (600–700) | Georgia, serif | Marca y títulos editoriales, cifras hero, títulos de marketing — nunca UI ni párrafos |
132
138
  | Body/UI | **Public Sans** (400–600) | system-ui, sans-serif | Párrafos, botones, formularios, h3–h6 |
133
139
  | Mono | **IBM Plex Mono** (400–500) | ui-monospace, monospace | Datos numéricos, código, IDs, montos en tablas |
134
140
 
@@ -173,6 +179,14 @@ Pesos tokenizados: `--weight-regular 400` · `--weight-medium 500` · `--weight-
173
179
 
174
180
  ---
175
181
 
182
+ ### Roles tipográficos por tarea
183
+
184
+ La etiqueta HTML expresa estructura, no una familia tipográfica. En **Operate** (formularios, datos, ajustes), usa `data-mode="operate"` en la región: la familia de UI lleva h1–h3, con una escala fija en rem de 32/20/18px. `.ui-title` permite aplicarla de forma local. Conserva un solo h1 de página y el orden de encabezados.
185
+
186
+ En **Read**, la familia display puede abrir una página editorial; `.reading-title` y `.display` consumen `--text-display-fluid`, con límites en rem y `text-wrap: balance`. El cuerpo mantiene 65–75ch y tamaños en rem. **Persuade** prioriza una decisión y **Experience** deja protagonismo a la obra: no copies la composición de un dashboard a una landing. Las cifras de tablas usan `tabular-nums`; reserva la mono para código, IDs o mediciones que lo necesiten.
187
+
188
+ ---
189
+
176
190
  ## 5. Espaciado
177
191
 
178
192
  Escala base 4px — tokens `--space-1…24` (número = px/4): `4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96`
@@ -190,6 +204,40 @@ Aliases de uso (consumir estos, no números sueltos):
190
204
 
191
205
  ---
192
206
 
207
+ ### Ritmo entre textos, controles y tarjetas
208
+
209
+ | Relación | Separación | Token |
210
+ |---|---:|---|
211
+ | Etiqueta → control; control → ayuda/error | 6px | `--field-gap` |
212
+ | Título → descripción del mismo grupo | 8px | `--heading-gap` |
213
+ | Párrafos o bloques relacionados | 16px | `--content-gap` |
214
+ | Bloques directos dentro de una tarjeta | 16px | `--card-content-gap` |
215
+ | Campos consecutivos y pie del formulario | 20px | `--form-gap` |
216
+ | Tarjetas consecutivas | 24px | `--card-group-gap` |
217
+ | Secciones independientes | 64px | `--section-gap` |
218
+
219
+ El padding protege el borde; el gap separa contenido. `.card` organiza sus hijos directos con `--card-content-gap`. Para anidar contenido usa `.card__content` o `.stack`; `.text-group` agrupa título y descripción a 8px. `.form-stack` separa campos a 20px; `.cluster` agrupa acciones con 12px y permite envolver. `.stack--cards` consume la separación de tarjetas de esta marca.
220
+
221
+ Un contenedor es dueño de la separación: dentro de stacks y grids se neutralizan los márgenes de sus hijos para no sumar margen y gap. Los `.field` consecutivos también se separan en flujo normal. Evita repartir márgenes manuales entre cada div; aplica el contenedor al grupo. Las alturas de control son mínimas: permiten crecer cuando aumenta el tamaño del texto.
222
+
223
+ ```html
224
+ <div class="stack stack--cards">
225
+ <section class="card" data-mode="operate">
226
+ <header class="text-group"><h2>Datos de contacto</h2><p>Usaremos estos datos para responder.</p></header>
227
+ <form class="form-stack">
228
+ <div class="field"><label class="field__label" for="nombre">Nombre</label><input class="input" id="nombre" autocomplete="name"></div>
229
+ <div class="field"><label class="field__label" for="correo">Correo</label><input class="input" id="correo" type="email" autocomplete="email"></div>
230
+ <div class="cluster"><button class="btn btn--primary" type="submit">Guardar</button><button class="btn btn--secondary" type="button">Cancelar</button></div>
231
+ </form>
232
+ </section>
233
+ <section class="card"><h2 class="ui-title">Otra sección</h2><p>El contenedor mantiene su distancia respecto a la tarjeta anterior.</p></section>
234
+ </div>
235
+ ```
236
+
237
+ Verifica la composición con ayuda/error multilínea, textos largos, 320px y texto al 200%. El texto debe envolver y los controles crecer; no reduzcas gaps para hacer caber contenido.
238
+
239
+ ---
240
+
193
241
  ## 6. Bordes, radios y focus
194
242
 
195
243
  | Token | Valor | Uso |
@@ -205,7 +253,7 @@ Aliases de uso (consumir estos, no números sueltos):
205
253
 
206
254
  ```css
207
255
  :where(a, button, [role="button"], input, select, textarea, summary, [tabindex]):focus-visible {
208
- outline: 2px solid var(--primary-500);
256
+ outline: 2px solid var(--focus-color);
209
257
  outline-offset: 2px;
210
258
  border-radius: 2px;
211
259
  }
@@ -269,11 +317,11 @@ No hay logo ni imágenes de marca — el wordmark es puramente tipográfico. Don
269
317
 
270
318
  ### 10.1 Botones
271
319
 
272
- Alturas: 36 (sm) / 44 (md) / 52px (lg). Radius `md`. Peso 600, `text-sm`/`text-base`; lg usa padding `14px 24px` y `text-lg`. Un solo primario visible por pantalla; ghost y outline usan `--link` (AA), no `primary-500`.
320
+ Alturas: 36 (sm) / 44 (md) / 52px (lg). Radius `md`. Peso 600, `text-sm`/`text-base`; lg usa padding `14px 24px` y `text-lg`. Una acción dominante por contexto de decisión; ghost y outline usan `--link` (AA), no `primary-500`.
273
321
 
274
322
  | Variante | Reposo | Hover | Active | Disabled |
275
323
  |---|---|---|---|---|
276
- | Primario | fondo `--accent`, texto `--text-on-primary` | `--accent-hover` + `shadow-sm` | `--accent-active`, sin sombra | fondo `neutral-200`, texto `--text-disabled` |
324
+ | Primario | fondo `--action-bg`, texto `--text-on-action` | `--action-hover` | `--action-active`, sin sombra | fondo `neutral-200`, texto `--text-disabled` |
277
325
  | Secundario | fondo `--surface`, borde `--border-strong`, texto `neutral-800` | fondo `neutral-50` | fondo `neutral-100` | opacidad 50% |
278
326
  | Outline | fondo transparente, borde `--accent`, **texto `--link`** | fondo `--accent-soft`, borde `--accent-hover` | fondo `--accent-soft-strong` | opacidad 50% |
279
327
  | Ghost | **texto `--link`** (`primary-700`) | fondo `--accent-soft` | fondo `--accent-soft-strong` | fondo `neutral-200`, texto `--text-disabled` |
@@ -313,7 +361,7 @@ Export propio (extraído del estado loading de Button). SVG 16px default: círcu
313
361
 
314
362
  ### 10.5 Textarea
315
363
 
316
- Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px. Contador de caracteres `text-xs` `--text-disabled` alineado a la derecha.
364
+ 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.
317
365
 
318
366
  ### 10.6 Select
319
367
 
@@ -372,7 +420,7 @@ Rango de valor único.
372
420
 
373
421
  - Contenedor: borde `--border`, radius `lg`, fondo `--surface`, ítems separados por borde interno.
374
422
  - Header: botón full-width, padding `16px 20px`, `text-base` medium `--text-primary`; chevron 18px `--text-muted` rota 180° (200ms). `aria-expanded`.
375
- - Panel: altura animada con `grid-template-rows` 0fr→1fr 250ms ease-out (sin reflow); contenido `text-sm --text-secondary`, padding `0 20px 16px`.
423
+ - Panel: altura animada con `grid-template-rows` 0fr→1fr 250ms ease-out (con layout local controlado); contenido `text-sm --text-secondary`, padding `0 20px 16px`.
376
424
  - Un panel abierto a la vez salvo `allowMultiple`; `defaultOpen` por ids.
377
425
 
378
426
  ### 10.15 Tooltip
@@ -387,7 +435,7 @@ Banner de estado **inline y persistente** (vs Toast, transitorio): errores de fo
387
435
 
388
436
  - 4 tonos: fondo suave semántico (`*-100`), radius `md`, padding `14px 16px`.
389
437
  - Icono semántico 20px en `*-500`: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`.
390
- - Título `text-sm` semibold y cuerpo `text-sm` en `--*-text` (cuerpo con opacidad 0.92).
438
+ - Título `text-sm` semibold y cuerpo `text-sm` en `--*-text`, a contraste completo (sin opacidad).
391
439
  - Botón cerrar opcional (× 16px en el color del tono) vía `onDismiss`. `role="status"`.
392
440
 
393
441
  ### 10.17 Toast
@@ -456,6 +504,26 @@ Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
456
504
  - Valor grande en `--font-mono text-3xl` semibold `--text-primary` (tracking -0.01em, line-height 1).
457
505
  - 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`).
458
506
 
507
+ #### Chip de métrica
508
+
509
+ 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.
510
+
511
+ - **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
512
+ - **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va semibold en `--link` con `tabular-nums`.
513
+ - **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.
514
+ - **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).
515
+
516
+ ```html
517
+ <div class="metric-chips">
518
+ <span class="metric-chip"><span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span></span>
519
+ <button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
520
+ <span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
521
+ </button>
522
+ </div>
523
+ ```
524
+
525
+ **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.
526
+
459
527
  ### 10.23 Pagination
460
528
 
461
529
  Navegador de páginas 1-indexado, bajo una `Table`.
@@ -472,6 +540,37 @@ Navegador de páginas 1-indexado, bajo una `Table`.
472
540
  - **Tabs:** borde inferior `--border`; tab activa con indicador 2px `--accent` que se desliza entre tabs (250ms).
473
541
  - **Breadcrumbs:** `text-sm` `--text-muted`, separador `/` (tipográfico), último ítem `--text-primary`.
474
542
 
543
+ #### Sidebar contraíble
544
+
545
+ 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.
546
+
547
+ - **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`.
548
+ - **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.
549
+ - **Ítem activo:** `.is-active` (fondo `--accent-soft-strong`, texto `--link`) más `aria-current="page"`.
550
+ - **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`.
551
+ - **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.
552
+ - **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).
553
+ - **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).
554
+
555
+ ```html
556
+ <nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
557
+ <div class="sidebar__group" role="group" aria-labelledby="navHoy">
558
+ <span class="sidebar__group-label" id="navHoy">Hoy</span>
559
+ <a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
560
+ </div>
561
+ <button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
562
+ <svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
563
+ </button>
564
+ </nav>
565
+
566
+ <!-- Solo por debajo de 768px -->
567
+ <button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
568
+ aria-expanded="false" aria-controls="navDrawer">…menu…</button>
569
+ <div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
570
+ ```
571
+
572
+ **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.
573
+
475
574
  ### 10.25 Skeleton y EmptyState
476
575
 
477
576
  - Skeleton: fondo `--surface-alt` con shimmer (gradiente que barre en 1.4s, loop). Radius igual al contenido que reemplaza.
@@ -541,10 +640,10 @@ Botones adyacentes con bordes compartidos para acciones relacionadas (barra de f
541
640
 
542
641
  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).
543
642
 
544
- - **`.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-expanded` + `aria-controls`; un `.datepicker__icon` de calendario 18px a la derecha (decorativo, `aria-hidden`).
643
+ - **`.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`).
545
644
  - **`.datepicker__calendar`** — panel flotante `role="dialog"` con `aria-label`, mismo patrón que `.popover__panel`. Variante `.datepicker--right` ancla a la derecha.
546
- - **`.calendar`** — dentro del panel: `.calendar__header` (mes/año en `.calendar__title` + botones prev/next con `#i-chevron-left/right`), `.calendar__weekdays` (7 encabezados `role="columnheader"` con `aria-label` completo: "lunes"…), `.calendar__grid` (`role="grid"`), y `.calendar__day` (botón `role="gridcell"`, `aria-label` de la fecha completa vía `Intl.DateTimeFormat` y `aria-selected`). Semana empieza en lunes.
547
- - **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-disabled`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
645
+ - **`.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.
646
+ - **Estados de día:** `.calendar__day--today` (semibold + anillo interior `--border-strong`), `.calendar__day--selected` (fondo `--action-bg`, texto `--text-on-action`), `.calendar__day--outside` (días de otro mes, `--text-muted`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
548
647
  - **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.
549
648
  - **Motion:** el panel aparece con fade + `scale(0.96→1)`, `--duration-base` `--ease-out`, bajo el techo `--duration-page`. Solo `transform`/`opacity`.
550
649
 
@@ -612,7 +711,7 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
612
711
  ### 11.2 Principios de movimiento
613
712
 
614
713
  1. **Entra rápido, sale más rápido** — La salida siempre es ~20% más corta que la entrada.
615
- 2. **Solo `transform` y `opacity`** — Nunca animar `width/height/top/left` (evita reflow). Excepciones controladas: width de Progress y `grid-template-rows` del Accordion.
714
+ 2. **Propiedades según coste y propósito** — Prefiere `transform` y `opacity` para desplazamiento y aparición. Color y sombra pueden acompañar un estado. Width de Progress y `grid-template-rows` del Accordion recalculan layout: mantenlos locales, cortos y medidos; no son animaciones gratis.
616
715
  3. **Distancias cortas** — Desplazamientos de entrada de 4–8px; nada "vuela" por la pantalla.
617
716
  4. **Jerarquía temporal** — En listas, stagger de 30ms por ítem (máximo 6 ítems con stagger).
618
717
  5. **La animación explica** — Todo movimiento responde a: ¿de dónde vino, qué cambió, a dónde se fue?
@@ -652,26 +751,21 @@ Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se f
652
751
 
653
752
  ### 11.5 Accesibilidad del movimiento
654
753
 
754
+ La preferencia reducida cambia los tokens de duración propios a 0ms y detiene spinner y skeleton. Los estados finales siguen visibles; el feedback de color de botones puede durar 80ms. No se acortan con un selector universal las animaciones de otras librerías. No uses eventos de animación como única condición para completar una operación o retirar un elemento.
755
+
655
756
  ```css
656
757
  @media (prefers-reduced-motion: reduce) {
657
- *, *::before, *::after {
658
- animation-duration: 0.01ms !important;
659
- animation-iteration-count: 1 !important;
660
- transition-duration: 0.01ms !important;
661
- scroll-behavior: auto !important;
758
+ :root {
759
+ --duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
760
+ --duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
662
761
  }
762
+ html { scroll-behavior: auto; }
763
+ .spinner, .skeleton, .skeleton::after { animation: none; }
764
+ .btn, .icon-btn { transform: none; transition: background-color 80ms linear, color 80ms linear; }
663
765
  }
664
766
  ```
665
767
 
666
- Este reset con `*` es el **piso de seguridad**: garantiza que ningún motion sobreviva si el usuario pide reducirlo, incluso en componentes que no se revisaron a mano. Donde el movimiento comunica información real (progreso, un check, un conteo) y eliminarlo del todo perjudicaría la comprensión, un componente puede sobrescribir el reset con un selector más específico y una duración corta (p. ej. `transition-duration: 80ms`) en vez de colapsar a 0.01ms — hoy ningún componente del sistema ejerce esta excepción; todos usan el piso global.
667
-
668
- **Gating de hover:** los efectos de hover que mueven o elevan (transform, box-shadow) van dentro de `@media (hover: hover) and (pointer: fine)` — en touch no hay hover real y el tap dispara hovers falsos que se quedan pegados hasta el siguiente toque. Los hover de solo color/fondo pueden quedar fuera: un cambio de color pegado no estorba.
669
-
670
- ```css
671
- @media (hover: hover) and (pointer: fine) {
672
- .card--interactive:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
673
- }
674
- ```
768
+ Los hover que desplazan o elevan solo se aplican con `(hover: hover) and (pointer: fine)`. Verifica la alternativa reducida con el mismo contenido y flujo, incluidos abrir, cerrar, cargar y confirmar. Los datos llegan con su valor final desde HTML: no hay valor final visible desde la carga.
675
769
 
676
770
  ### 11.6 Anti-patrones de motion
677
771
 
@@ -705,7 +799,7 @@ Un "motion gap" es UI que aparece o desaparece sin transición y debería tenerl
705
799
  - Estados de carga y de error.
706
800
  - Paneles de settings / inspector.
707
801
 
708
- Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria, medio. Toda entrada/salida condicional necesita su transición de `opacity`/`transform`, con su contraparte de salida más discreta.
802
+ La ausencia de transición no es un defecto por sí sola. Evalúa si el cambio se entiende y mantiene el foco. En tareas frecuentes, teclado o movimiento reducido, una actualización instantánea puede ser la respuesta correcta.
709
803
 
710
804
  ---
711
805
 
@@ -714,7 +808,7 @@ Regla: un gap en UI primaria (el flujo principal) es crítico; en UI secundaria,
714
808
  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.
715
809
 
716
810
  - `neutral-800` sobre blanco: **12.42:1** ✅ AAA.
717
- - Blanco sobre `primary-500`: **3.22:1** ⚠️ solo texto ≥18px/bold o componentes UI; para texto normal usar fondo `primary-700`.
811
+ - Blanco sobre `primary-500`: **3.22:1** ⚠️ no apto para el texto normal de un botón; el rol `--action-bg` usa `primary-700` en claro.
718
812
  - `--link` (`primary-700`) sobre blanco: **5.94:1** ✅ AA — por eso links y ghost usan `--link`, no `primary-500`.
719
813
  - `primary-300` (#a0c0d4) **nunca como texto sobre blanco** — solo decorativo.
720
814
  - 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**.
@@ -728,20 +822,30 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
728
822
  ## 13. Tokens CSS consolidados
729
823
 
730
824
  ```css
825
+ /* ============================================================
826
+ Celeste Clásica — Design Tokens v2.0
827
+ Sistema de diseño de Higinio Sosa · light-first
828
+ Fuente de verdad: DESIGN-celeste-clasica.md
829
+ Uso: importar ANTES de cualquier estilo propio.
830
+ Los componentes consumen SOLO aliases semánticos, nunca hex.
831
+ ============================================================ */
832
+
833
+ @import url("https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,600;0,9..144,700;1,9..144,600&family=Public+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono:wght@400;500&display=swap");
834
+
731
835
  :root {
732
- /* == Primitivos: primario == */
836
+ /* == Primitivos: primario (Azul Acero) == */
733
837
  --primary-50: #f2f6fa; --primary-100: #dfe9f2; --primary-200: #c3d5e6;
734
838
  --primary-300: #a0c0d4; --primary-400: #87a8c7; --primary-500: #7192ba;
735
839
  --primary-600: #5d7ba3; --primary-700: #4d6588; --primary-800: #40536e;
736
840
  --primary-900: #31343e;
737
841
 
738
- /* == Primitivos: neutrales == */
842
+ /* == Primitivos: neutrales (Gris Azulado) == */
739
843
  --neutral-0: #ffffff; --neutral-50: #f7f8fa; --neutral-100: #eef0f3;
740
844
  --neutral-200: #dcdfe5; --neutral-300: #b8bdc7; --neutral-400: #8a8f9c;
741
845
  --neutral-500: #666b78; --neutral-600: #4d5160; --neutral-700: #3d414e;
742
846
  --neutral-800: #31343e; --neutral-900: #22242b;
743
847
 
744
- /* == Primitivos: semánticos (ramp de 3) == */
848
+ /* == Primitivos: semánticos (ramp de 3: sólido / fondo / texto AA) == */
745
849
  --success-500: #3d9970; --success-100: #e2f3ec; --success-text: #2b6e50;
746
850
  --warning-500: #c9922e; --warning-100: #faf1de; --warning-text: #8a651c;
747
851
  --danger-500: #c04d4d; --danger-100: #f9e7e7; --danger-text: #8f3232;
@@ -764,7 +868,7 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
764
868
  --accent-active: var(--primary-700);
765
869
  --accent-soft: var(--primary-50);
766
870
  --accent-soft-strong: var(--primary-100);
767
- --link: var(--primary-700);
871
+ --link: var(--primary-700); /* AA ~6:1 — texto de links y ghost; primary-500 solo para UI */
768
872
 
769
873
  /* == Tipografía == */
770
874
  --font-display: "Fraunces", Georgia, serif;
@@ -780,7 +884,7 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
780
884
  --weight-regular: 400; --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700;
781
885
  --measure: 65ch;
782
886
 
783
- /* == Espaciado == */
887
+ /* == Espaciado (escala base 4px) == */
784
888
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
785
889
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
786
890
  --space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
@@ -790,7 +894,7 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
790
894
  --control-height-sm: 36px; --control-height-md: 44px; --control-height-lg: 52px;
791
895
  --gutter: 24px; --gutter-mobile: 16px;
792
896
 
793
- /* == Radios, bordes y sombras == */
897
+ /* == Radios, bordes y sombras (teñidas de tinta, nunca negro puro) == */
794
898
  --radius-sm: 6px; --radius-md: 10px; --radius-lg: 14px;
795
899
  --radius-xl: 20px; --radius-full: 9999px;
796
900
  --border-width: 1px;
@@ -809,10 +913,112 @@ Ratios calculados con la fórmula WCAG 2.x (no estimados). El test `mcp/test/con
809
913
  --duration-slow: 250ms; --duration-page: 320ms;
810
914
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
811
915
  --ease-in: cubic-bezier(0.4, 0, 1, 1);
812
- --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
916
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* moverse de A a B en pantalla */
813
917
  --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
918
+ --stagger-step: 30ms;
814
919
 
815
920
  color-scheme: light;
921
+
922
+ --overlay: rgba(49, 52, 62, .5);
923
+ --tooltip-bg: var(--neutral-900);
924
+ --tooltip-text: #ffffff;
925
+ --ring-danger: 0 0 0 3px rgba(192,77,77,.22);
926
+
927
+ --action-bg: var(--primary-700);
928
+ --action-hover: var(--primary-800);
929
+ --action-active: var(--primary-900);
930
+ --text-on-action: var(--text-on-primary);
931
+ --control-border: var(--neutral-400);
932
+ --placeholder: var(--text-muted);
933
+ --selection-bg: var(--action-bg);
934
+ --selection-text: var(--text-on-action);
935
+ --font-ui-heading: var(--font-body);
936
+ --text-ui-title: 2rem;
937
+ --touch-target: 44px;
938
+ --focus-color: var(--accent);
939
+ --text-display-fluid: clamp(2.75rem, 1.7rem + 3vw, 4.75rem);
940
+ --border-hover: var(--neutral-500);
941
+ --text-on-danger: #ffffff;
942
+
943
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
944
+ --chart-1: #4a8cc1;
945
+ --chart-2: #853f63;
946
+ --chart-3: #129a92;
947
+ --chart-4: #8e4031;
948
+ --chart-5: #5b975e;
949
+ --chart-6: #8a600a;
950
+ --chart-7: #857bc1;
951
+ /* Ritmo de contenido y formularios. */
952
+ --field-gap: 6px;
953
+ --heading-gap: var(--space-2);
954
+ --content-gap: var(--space-4);
955
+ --card-content-gap: var(--space-4);
956
+ --card-group-gap: var(--space-6);
957
+
958
+ }
959
+
960
+ /* == Modo oscuro (variante) — alternar data-theme en <html> == */
961
+ [data-theme="dark"] {
962
+ --bg: #16181e;
963
+ --surface: #1d2028;
964
+ --surface-alt: #252935;
965
+ --border: #3a3f4d;
966
+ --border-strong: #4a5060;
967
+ --text-primary: #e6e9ee;
968
+ --text-secondary: #b9bfcb;
969
+ --text-muted: #8b91a0;
970
+ --text-disabled: #6f7585;
971
+ --accent: #87a8c7;
972
+ --accent-hover: #a0c0d4;
973
+ --accent-active: #7192ba;
974
+ --accent-soft: rgba(113,146,186,.12);
975
+ --accent-soft-strong: rgba(113,146,186,.18);
976
+ --text-on-primary: #16181e;
977
+ --link: #a0c0d4;
978
+ --success-500: #5cbf93; --success-100: #12291f; --success-text: #7fd4ae;
979
+ --warning-500: #e0ad4d; --warning-100: #2b2210; --warning-text: #ecc57e;
980
+ --danger-500: #e07777; --danger-100: #2e1515; --danger-text: #eb9e9e;
981
+ --info-500: #87a8c7; --info-100: #1a2430; --info-text: #a0c0d4;
982
+ --ring: 0 0 0 3px rgba(160,192,212,.3);
983
+ --ring-color: rgba(160,192,212,.3);
984
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.35);
985
+ --shadow-md: 0 4px 12px rgba(0,0,0,.45);
986
+ --shadow-lg: 0 12px 32px rgba(0,0,0,.55);
987
+ color-scheme: dark;
988
+
989
+ --text-on-primary-solid: #a0c0d4;
990
+ --overlay: rgba(0, 0, 0, .6);
991
+ --tooltip-bg: #e6e9ee;
992
+ --tooltip-text: #31343e;
993
+ --ring-danger: 0 0 0 3px rgba(224,119,119,.25);
994
+
995
+ --selection-bg: var(--action-bg);
996
+ --selection-text: var(--text-on-action);
997
+ --action-bg: var(--accent);
998
+ --action-hover: var(--accent-hover);
999
+ --action-active: var(--accent-active);
1000
+ --control-border: var(--text-muted);
1001
+ --border-hover: var(--text-secondary);
1002
+ --text-on-danger: #16181e;
1003
+ --focus-color: var(--accent);
1004
+
1005
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
1006
+ --chart-1: #5798ce;
1007
+ --chart-2: #a0587c;
1008
+ --chart-3: #2aa79e;
1009
+ --chart-4: #aa5849;
1010
+ --chart-5: #67a369;
1011
+ --chart-6: #946a1c;
1012
+ --chart-7: #877cc3;
1013
+ }
1014
+
1015
+ /* == Accesibilidad del movimiento == */
1016
+
1017
+ @media (prefers-reduced-motion: reduce) {
1018
+ :root {
1019
+ --duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
1020
+ --duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
1021
+ }
816
1022
  }
817
1023
  ```
818
1024
 
@@ -839,6 +1045,17 @@ Grid de 12 columnas, gutter 24px (16px móvil). Mobile-first. Navbar fija de 64p
839
1045
 
840
1046
  ---
841
1047
 
1048
+ ### Adaptación de los componentes
1049
+
1050
+ - Reflow a 320 CSS px sin recortar contenido ni ocultar el desbordamiento del documento. Revisa también texto al 200% y zoom al 400%.
1051
+ - Acciones de formularios se apilan cuando no caben; el primario puede ocupar el ancho disponible en móvil. Las etiquetas largas pueden envolver sin quedar cortadas.
1052
+ - La navegación lateral se convierte en disclosure o drawer con estado accesible, cierre con Escape, devolución de foco y contenido cerrado fuera del orden de tabulación. Tabs largas conservan desplazamiento horizontal local.
1053
+ - Una tabla ancha usa `.table-scroll`, con nombre accesible y región enfocables si el scroll necesita teclado; prioriza columnas o una vista de detalle sin retirar datos necesarios.
1054
+ - Calendarios y grupos compactos mantienen objetivos de al menos 24×24px o espaciado equivalente; 44×44px es la preferencia de comodidad táctil, no el mínimo AA universal. El tamaño visible del icono puede ser menor que su zona activa.
1055
+ - Popovers, combobox y calendarios respetan el viewport y se reposicionan cerca de sus bordes. En producción usa top layer, portal o posicionamiento con detección de colisiones para escapar de ancestros con overflow. El framework CSS por sí solo no gestiona colisiones ni foco.
1056
+
1057
+ ---
1058
+
842
1059
  ## 15. Modo oscuro (variante)
843
1060
 
844
1061
  El sistema es **light-first**, pero todo producto debe soportar dark mode con los mismos tokens semánticos. La marca no cambia: cambia el escenario.
@@ -918,6 +1135,8 @@ Ratios calculados, no estimados (mismo test que §12).
918
1135
  --accent: #87a8c7;
919
1136
  --accent-hover: #a0c0d4;
920
1137
  --accent-active: #7192ba;
1138
+ --accent-soft: rgba(113,146,186,.12);
1139
+ --accent-soft-strong: rgba(113,146,186,.18);
921
1140
  --text-on-primary: #16181e;
922
1141
  --link: #a0c0d4;
923
1142
  --success-500: #5cbf93; --success-100: #12291f; --success-text: #7fd4ae;
@@ -925,10 +1144,36 @@ Ratios calculados, no estimados (mismo test que §12).
925
1144
  --danger-500: #e07777; --danger-100: #2e1515; --danger-text: #eb9e9e;
926
1145
  --info-500: #87a8c7; --info-100: #1a2430; --info-text: #a0c0d4;
927
1146
  --ring: 0 0 0 3px rgba(160,192,212,.3);
1147
+ --ring-color: rgba(160,192,212,.3);
928
1148
  --shadow-sm: 0 1px 2px rgba(0,0,0,.35);
929
1149
  --shadow-md: 0 4px 12px rgba(0,0,0,.45);
930
1150
  --shadow-lg: 0 12px 32px rgba(0,0,0,.55);
931
1151
  color-scheme: dark;
1152
+
1153
+ --text-on-primary-solid: #a0c0d4;
1154
+ --overlay: rgba(0, 0, 0, .6);
1155
+ --tooltip-bg: #e6e9ee;
1156
+ --tooltip-text: #31343e;
1157
+ --ring-danger: 0 0 0 3px rgba(224,119,119,.25);
1158
+
1159
+ --selection-bg: var(--action-bg);
1160
+ --selection-text: var(--text-on-action);
1161
+ --action-bg: var(--accent);
1162
+ --action-hover: var(--accent-hover);
1163
+ --action-active: var(--accent-active);
1164
+ --control-border: var(--text-muted);
1165
+ --border-hover: var(--text-secondary);
1166
+ --text-on-danger: #16181e;
1167
+ --focus-color: var(--accent);
1168
+
1169
+ /* Series categóricas: orden estable; acompañar con etiqueta o forma. */
1170
+ --chart-1: #5798ce;
1171
+ --chart-2: #a0587c;
1172
+ --chart-3: #2aa79e;
1173
+ --chart-4: #aa5849;
1174
+ --chart-5: #67a369;
1175
+ --chart-6: #946a1c;
1176
+ --chart-7: #877cc3;
932
1177
  }
933
1178
  ```
934
1179
 
@@ -955,3 +1200,11 @@ El sistema se distribuye en varios formatos para poder usarlo desde cualquier st
955
1200
  3. Los componentes consumen solo aliases semánticos (§3.4) — nunca hex.
956
1201
  4. Para light/dark: alternar `data-theme` en `<html>`; los tokens hacen el resto.
957
1202
  5. En el `<head>`: `<meta name="theme-color" content="#f7f8fa">` (el `--bg` claro); con el tema oscuro activo, actualizarlo a `#16181e` para que la barra del navegador acompañe al fondo.
1203
+
1204
+ ### Mantenimiento de las distribuciones
1205
+
1206
+ Los contratos se definen en esta especificación y sus valores ejecutables en `dist/celeste-clasica/tokens.css`. Los ajustes comunes de componentes están en `web/component-contracts.css`; la presentación compartida de las guías, en `web/styleguide.css`. Tras editar, ejecuta `node scripts/build-design-assets.mjs`: regenera el prefijo de tokens del framework, el CSS embebido de las guías y los roles semánticos de Tailwind/DTCG. Los componentes propios del framework se conservan. Luego sincroniza skills, paquete CSS y MCP. Las guías siguen siendo HTML autocontenido, excepto la carga externa de fuentes.
1207
+
1208
+ ### Series categóricas
1209
+
1210
+ `--chart-1` a `--chart-7` mantienen su orden y cambian de paso con el tema. La prueba `mcp/test/chart-palettes.test.ts` verifica contraste ≥3:1 contra la superficie y separación entre colores adyacentes en OKLab: ≥15 en visión normal y ≥8 bajo simulaciones Machado de protanopía, deuteranopía y tritanopía. Son umbrales internos, no certificación WCAG ni garantía para todos los pares o personas; usa etiquetas, formas o patrones y tabla equivalente.