@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.
- package/LICENSE +4 -4
- package/README.md +5 -5
- package/assets/bruma/design-doc.md +1275 -0
- package/assets/bruma/framework.css +1591 -0
- package/assets/bruma/styleguide.html +5195 -0
- package/assets/bruma/tailwind.css +163 -0
- package/assets/bruma/tokens.css +203 -0
- package/assets/bruma/tokens.json +1739 -0
- package/assets/celeste/design-doc.md +297 -44
- package/assets/celeste/framework.css +382 -65
- package/assets/celeste/styleguide.html +1011 -196
- package/assets/celeste/tailwind.css +48 -6
- package/assets/celeste/tokens.css +67 -6
- package/assets/celeste/tokens.json +778 -26
- package/assets/nocturna/design-doc.md +406 -61
- package/assets/nocturna/framework.css +418 -114
- package/assets/nocturna/styleguide.html +1147 -330
- package/assets/nocturna/tailwind.css +46 -3
- package/assets/nocturna/tokens.css +123 -9
- package/assets/nocturna/tokens.json +754 -21
- package/assets/salvia/design-doc.md +1260 -0
- package/assets/salvia/framework.css +1588 -0
- package/assets/salvia/styleguide.html +5176 -0
- package/assets/salvia/tailwind.css +163 -0
- package/assets/salvia/tokens.css +199 -0
- package/assets/salvia/tokens.json +1767 -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/snippet.js +26 -17
- package/dist/systems.js +27 -1
- package/dist/tokens.js +42 -5
- package/dist/validate.js +13 -5
- package/package.json +5 -3
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
# DESIGN.md — Sistema de Diseño "Salvia Serena"
|
|
2
|
+
|
|
3
|
+
> Sistema de marca **light-first**, natural y relajante, de **Higinio Sosa** para sus productos de bienestar, hábitos y salud (web, móvil, dashboards, landing pages).
|
|
4
|
+
> Paleta base: `#f3f2ec` (lino, fondo) · `#4f6b58` (salvia, primario) · `#2c3630` (tinta verde)
|
|
5
|
+
>
|
|
6
|
+
> **Versión 1.0**.
|
|
7
|
+
> Entregables: `salvia-serena.html` (style guide), `dist/salvia-serena/` (tokens CSS · framework · Tailwind v4 · DTCG JSON), skill `~/.claude/skills/salvia-serena-design/`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Identidad y principios
|
|
12
|
+
|
|
13
|
+
**Personalidad de marca:** natural, relajante y cálida, sin ser rústica. Transmite confort y control sin presión; la calma viene del color, del aire y de un ritmo pausado, no de la decoración. Vibe: *«Lino al sol y hojas de salvia; nada urge»*.
|
|
14
|
+
|
|
15
|
+
1. **Calma antes que urgencia** — Nada parpadea, nada apremia. El sistema acompaña; nunca presiona ni cuenta hacia atrás.
|
|
16
|
+
2. **Claridad primero** — El contenido siempre por encima de la decoración.
|
|
17
|
+
3. **Un acento a la vez** — El salvia `#4f6b58` guía la acción; nunca compite consigo mismo y **nunca comunica éxito**.
|
|
18
|
+
4. **La naturalidad la ponen el color y la tipografía** — Lino, salvia y una serif cálida (Lora). Sin hojas dibujadas, sin texturas de papel, sin motivos botánicos de adorno.
|
|
19
|
+
5. **Movimiento pausado y con propósito** — Algo más lento que lo habitual, porque relaja; la animación explica (aparece, cambia, desaparece), nunca decora sola.
|
|
20
|
+
|
|
21
|
+
**Reglas duras propias:**
|
|
22
|
+
|
|
23
|
+
- **El acento salvia nunca comunica éxito.** El éxito es verde azulado (`--success-500` `#2f7d6a`) y **siempre va con icono ✓** y texto. Un botón salvia y una comida «completada» no pueden verse iguales.
|
|
24
|
+
- **Nada de ilustraciones de hojas ni texturas de papel.** Ni granulado, ni fondos con fibra, ni ramitas en los encabezados: la naturalidad la dan el color y la tipografía.
|
|
25
|
+
|
|
26
|
+
**Regla de oro de implementación:** los componentes consumen **solo tokens semánticos** (`--surface`, `--accent`, `--text-primary`, `--link`…), nunca hex directos. El theme switch es un cambio de atributo, no un refactor.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Voz y contenido
|
|
31
|
+
|
|
32
|
+
- **Idioma:** español (México/LatAm neutro) — "tú", nunca "usted".
|
|
33
|
+
- **Tono:** sereno, cercano y natural. Frases cortas y amables; cálido sin ser empalagoso, natural sin ser rústico (nada de "cosecha", "huertito" ni diminutivos). **Cero signos de exclamación** y cero jerga de marketing.
|
|
34
|
+
- **Sin urgencia:** nunca "¡Rápido!", "Última oportunidad", "No te quedes atrás" ni contadores que presionan. Si algo tiene plazo, se dice con calma: "Puedes registrarlo hasta las 23:00".
|
|
35
|
+
- **Sin culpa:** el progreso se cuenta por lo hecho, no por lo que falta. "3 de 8 vasos", no "Te faltan 5". Un día sin registros no se reprocha.
|
|
36
|
+
- **Voz:** segunda persona ("tu comida", "tus hábitos"), nunca primera persona del producto ("nosotros").
|
|
37
|
+
- **Casing:** sentence case en todo — botones, headings, labels. Nunca Title Case ni ALL CAPS, salvo labels uppercase muy puntuales (`+0.04em` tracking) para eyebrows/headers de tabla.
|
|
38
|
+
- **Botones:** 1–3 palabras, imperativos y tranquilos ("Guardar", "Registrar comida", "Más tarde").
|
|
39
|
+
- **Errores:** explican qué falló **y qué hacer**, sin dramatizar: "No se pudo guardar. Revisa tu conexión e inténtalo de nuevo."
|
|
40
|
+
- **Estados vacíos:** invitan a una acción concreta, sin presionar — "Aún no registras comidas hoy. Añade la primera cuando quieras." Nunca un simple "No hay datos".
|
|
41
|
+
- **Logros:** se confirman con sobriedad ("Meta del día cumplida"), con el tono de éxito y su ✓; sin euforia, sin confeti.
|
|
42
|
+
- **Emoji:** no se usan. El estado se comunica con color + icono + texto.
|
|
43
|
+
- **Fechas y números:** siempre con `Intl.DateTimeFormat` / `Intl.NumberFormat` (`es-MX`), nunca formatos hardcodeados ("12/07/2026" a mano se rompe al cambiar de locale).
|
|
44
|
+
- **`translate="no"`** en nombres de marca y tokens de código, para que la traducción automática no los corrompa.
|
|
45
|
+
- **Numerales para conteos:** "8 vasos", no "ocho vasos" — se escanean más rápido.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 3. Paleta de colores
|
|
50
|
+
|
|
51
|
+
### 3.1 Primario — Salvia (base `#4f6b58`)
|
|
52
|
+
|
|
53
|
+
| Token | Hex | Uso |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `primary-50` | `#e2eadf` | **Acento suave** — hover de ghost/outline, fila seleccionada, chips |
|
|
56
|
+
| `primary-100` | `#d4e0d1` | Seleccionados suaves, ítem activo del sidebar, chip de avatar |
|
|
57
|
+
| `primary-200` | `#b8cbb7` | Bordes activos suaves, divisores destacados |
|
|
58
|
+
| `primary-300` | `#97b09e` | Decorativo — iconos de empty state, gráficas. **Nunca texto** (2.23:1) |
|
|
59
|
+
| `primary-400` | `#73907c` | Iconos secundarios, hover de checkbox/radio (solo UI) |
|
|
60
|
+
| `primary-500` | `#4f6b58` | **Primario** — fondo de botones, bordes activos, focus, rellenos de gráficas |
|
|
61
|
+
| `primary-600` | `#435c4b` | Hover de botones primarios, **`--link`: texto de links y botones ghost** |
|
|
62
|
+
| `primary-700` | `#384d3f` | Pressed/active, tabs y navbar activos, texto sobre `primary-100` |
|
|
63
|
+
| `primary-800` | `#304236` | Texto de marca con alto contraste |
|
|
64
|
+
| `primary-900` | `#2c3630` | **Tinta de marca** — headings, texto principal |
|
|
65
|
+
|
|
66
|
+
### 3.2 Neutrales — Lino (derivados de `#2c3630`)
|
|
67
|
+
|
|
68
|
+
| Token | Hex | Uso |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `neutral-0` | `#fbfaf6` | Superficie de tarjetas (lino claro, no blanco puro) |
|
|
71
|
+
| `neutral-50` | `#f3f2ec` | **Lino** — fondo general de la app |
|
|
72
|
+
| `neutral-100` | `#eae9e1` | Fondos alternos, filas zebra, tracks |
|
|
73
|
+
| `neutral-200` | `#dfe0d6` | Bordes y divisores por defecto |
|
|
74
|
+
| `neutral-300` | `#b9beb4` | Iconos deshabilitados, presencia "offline" |
|
|
75
|
+
| `neutral-400` | `#858d84` | Bordes de inputs y botones secundarios, placeholder, texto deshabilitado |
|
|
76
|
+
| `neutral-500` | `#5c675f` | Texto terciario, captions |
|
|
77
|
+
| `neutral-600` | `#47524b` | Texto secundario |
|
|
78
|
+
| `neutral-700` | `#38423b` | Párrafos largos |
|
|
79
|
+
| `neutral-800` | `#2c3630` | Texto principal, headings |
|
|
80
|
+
| `neutral-900` | `#1f2622` | Texto de máximo énfasis, fondo de tooltip |
|
|
81
|
+
|
|
82
|
+
### 3.3 Semánticos (ramp de 3 tonos por estado)
|
|
83
|
+
|
|
84
|
+
Cada estado tiene tres tokens: el sólido (`-500`), el fondo suave (`-100`) y **el tono de texto AA sobre el fondo suave (`-text`)**. Atención y error son tonos tierra; info es un azul pizarra propio, para no diluir el acento.
|
|
85
|
+
|
|
86
|
+
| Estado | Sólido `-500` | Fondo `-100` | Texto `-text` | Uso |
|
|
87
|
+
|---|---|---|---|---|
|
|
88
|
+
| `success` | `#2f7d6a` | `#dcefe8` | `#22604f` | Verde azulado — confirmaciones, metas cumplidas, siempre con ✓ |
|
|
89
|
+
| `warning` | `#b8862f` | `#f6ecd8` | `#7e5a17` | Ámbar tierra — avisos, pendientes |
|
|
90
|
+
| `danger` | `#b5523f` | `#f6e4df` | `#8a3a2b` | Rojo arcilla — errores, acciones destructivas |
|
|
91
|
+
| `info` | `#5b7891` | `#e3eaf0` | `#3f5a70` | Azul pizarra — informativos |
|
|
92
|
+
|
|
93
|
+
### 3.4 Capa de aliases semánticos
|
|
94
|
+
|
|
95
|
+
Los componentes **nunca** usan los primitivos directamente: consumen esta capa.
|
|
96
|
+
|
|
97
|
+
| Alias | Valor | Rol |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `--bg` | `neutral-50` | Fondo de la app (lino) |
|
|
100
|
+
| `--surface` | `neutral-0` | Tarjetas, paneles |
|
|
101
|
+
| `--surface-alt` | `neutral-100` | Fondos alternos, zebra, tracks |
|
|
102
|
+
| `--border` | `neutral-200` | Borde por defecto |
|
|
103
|
+
| `--border-strong` | `neutral-400` | Bordes de inputs y botones secundarios (3.27:1) |
|
|
104
|
+
| `--text-primary` | `neutral-800` | Texto principal |
|
|
105
|
+
| `--text-secondary` | `neutral-600` | Texto secundario |
|
|
106
|
+
| `--text-muted` | `neutral-500` | Terciario, captions |
|
|
107
|
+
| `--text-disabled` | `neutral-400` | Solo controles deshabilitados |
|
|
108
|
+
| `--text-on-primary` | `#ffffff` | Texto sobre botón primario |
|
|
109
|
+
| `--text-on-primary-solid` | `primary-700` | Texto de peso normal sobre `primary-100` |
|
|
110
|
+
| `--accent` | `primary-500` | Rellenos de acción, bordes activos, focus |
|
|
111
|
+
| `--accent-hover` | `primary-600` | Hover del primario |
|
|
112
|
+
| `--accent-active` | `primary-700` | Pressed |
|
|
113
|
+
| `--accent-soft` | `primary-50` | Hover de ghost, fondos sutiles |
|
|
114
|
+
| `--accent-soft-strong` | `primary-100` | Seleccionados suaves |
|
|
115
|
+
| `--link` | `primary-600` | **Texto de links y botones ghost (AA 7.00:1)** |
|
|
116
|
+
| `--ring` | `0 0 0 3px rgba(79,107,88,.25)` | Focus ring de inputs |
|
|
117
|
+
|
|
118
|
+
> ⚠️ **Regla AA de acento:** `primary-500` sobre `--surface` da 5.62:1, pero el texto de links y ghost usa siempre `--link` (`primary-600`, 7.00:1) para que cada rol tenga su token. `primary-400` (3.35:1) solo en iconos y UI; `primary-300` (2.23:1) jamás como texto.
|
|
119
|
+
|
|
120
|
+
> ⚠️ **Regla de éxito:** el acento salvia **nunca comunica éxito**. Un resultado positivo usa `--success-*` (verde azulado) + icono ✓ (`check` o `circle-check`) + texto. La distancia de color entre `--accent` y `--success-500` es ΔE76 15.5 en claro y 16.2 en oscuro; el test de contraste exige al menos 15, así que esos dos hex no pueden acercarse más.
|
|
121
|
+
|
|
122
|
+
### 3.5 Uso del color y jerarquía
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
| Rol semántico | Uso |
|
|
127
|
+
|---|---|
|
|
128
|
+
| `--action-bg`, `--action-hover`, `--action-active` | Relleno de acciones sólidas y selección con texto |
|
|
129
|
+
| `--text-on-action` | Texto e iconos sobre los tres fondos de acción; ≥4.5:1 |
|
|
130
|
+
| `--control-border` | Contorno necesario para reconocer campos, checkbox y radio; ≥3:1 |
|
|
131
|
+
| `--focus-color` | Contorno sólido de teclado, distinto del halo decorativo |
|
|
132
|
+
| `--placeholder` | Ayuda legible dentro de campos; nunca sustituye al label |
|
|
133
|
+
| `--selection-bg`, `--selection-text` | Selección nativa de texto, con contraste propio |
|
|
134
|
+
|
|
135
|
+
`--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`.
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 4. Tipografía
|
|
142
|
+
|
|
143
|
+
### 4.1 Familias y funciones
|
|
144
|
+
|
|
145
|
+
| Rol | Fuente | Fallback | Uso |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| Display | **Lora** (600–700) | Georgia, serif | Marca y títulos editoriales, cifras hero, títulos de marketing — nunca UI ni párrafos |
|
|
148
|
+
| Body/UI | **Nunito Sans** (400–700) | system-ui, sans-serif | Párrafos, botones, formularios, h3–h6 |
|
|
149
|
+
| Mono | **Source Code Pro** (400–500) | ui-monospace, monospace | Datos numéricos, código, IDs, cantidades en tablas |
|
|
150
|
+
|
|
151
|
+
Lora aporta la calidez de una serif caligráfica contenida; Nunito Sans, de terminales suaves, mantiene la UI amable y legible. La itálica de Lora (600) queda para un énfasis editorial puntual en display —una cita, un subtítulo de portada o el descriptor del wordmark (§9)—, nunca en UI ni en párrafos.
|
|
152
|
+
|
|
153
|
+
Carga vía Google Fonts CDN (familias exactas del brief, no sustitutas — si llegan binarios licenciados, sustituir por `@font-face` con los mismos `font-family`, cero cambios downstream):
|
|
154
|
+
|
|
155
|
+
```css
|
|
156
|
+
@import url("https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,600;0,700;1,600&family=Nunito+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Source+Code+Pro:wght@400;500&display=swap");
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 4.2 Escala (ratio 1.25 — tercera mayor, base 16px, en `rem`)
|
|
160
|
+
|
|
161
|
+
| Token | Tamaño | Line-height (token) | Peso | Uso |
|
|
162
|
+
|---|---|---|---|---|
|
|
163
|
+
| `text-xs` | 12px | 1.5 `--leading-xs` | 400–500 | Captions, labels |
|
|
164
|
+
| `text-sm` | 14px | 1.5 `--leading-sm` | 400 | Texto secundario, tablas |
|
|
165
|
+
| `text-base` | 16px | 1.6 `--leading-base` | 400 | Párrafos, inputs |
|
|
166
|
+
| `text-lg` | 18px | 1.55 `--leading-lg` | 500 | Lead, subtítulos |
|
|
167
|
+
| `text-xl` | 20px | 1.4 `--leading-xl` | 600 | h4 |
|
|
168
|
+
| `text-2xl` | 25px | 1.3 `--leading-2xl` | 600 | h3 (Nunito Sans) |
|
|
169
|
+
| `text-3xl` | 31px | 1.25 `--leading-3xl` | 600 | h2 (Lora) |
|
|
170
|
+
| `text-4xl` | 39px | 1.15 `--leading-4xl` | 700 | h1 (Lora) |
|
|
171
|
+
| `text-5xl` | 49px | 1.1 `--leading-5xl` | 700 | Display/hero |
|
|
172
|
+
|
|
173
|
+
Pesos tokenizados: `--weight-regular 400` · `--weight-medium 500` · `--weight-semibold 600` · `--weight-bold 700`.
|
|
174
|
+
|
|
175
|
+
### 4.3 Reglas
|
|
176
|
+
|
|
177
|
+
- Letter-spacing tokenizado: `--tracking-display: -0.02em` (display ≥31px), `--tracking-body: 0`, `--tracking-label: +0.04em` (labels uppercase).
|
|
178
|
+
- Longitud de línea: `--measure: 65ch` (60–75 caracteres).
|
|
179
|
+
- Números en tablas: `font-variant-numeric: tabular-nums` o Source Code Pro.
|
|
180
|
+
- Mínimo absoluto: 12px con line-height 1.5.
|
|
181
|
+
|
|
182
|
+
### 4.4 Jerarquía y craft (Butterick)
|
|
183
|
+
|
|
184
|
+
- **Máx. 3 niveles de heading visualmente distintos por vista.** La escala define tamaños para h1–h4, pero en una misma pantalla no uses más de tres niveles claramente diferenciados; el exceso aplana la jerarquía.
|
|
185
|
+
- **Jerarquía por espacio y peso, no solo por tamaño.** Deja más espacio arriba de un heading que abajo (lo ata a su contenido). Distingue niveles con espacio y peso, no solo con puntos de tamaño.
|
|
186
|
+
- **Comillas y apóstrofos curvos** (« », " ", ' '), nunca rectos (`"`, `'`). Un solo espacio tras el punto seguido (nunca doble, herencia de máquina de escribir).
|
|
187
|
+
- **Interlineado generoso en cuerpo de texto: 150–160%** (ya son los tokens `--leading-base`/`--leading-sm`, §4.2); no bajes de 130% ni en texto denso (tablas, captions).
|
|
188
|
+
- **En dash (–) para rangos y series** ("10–20 minutos", "lun–vie"); **em dash (—) solo para incisos**, con moderación (la skill `anti-slop-diseno` marca >2 por bloque como cliché de IA).
|
|
189
|
+
- **Kerning activado** (`font-kerning: normal`); ligaduras por defecto.
|
|
190
|
+
- En JSX/TSX y en strings usa el carácter UTF-8 real (`'`, `"`), no escapes ni entidades (`’`, `’`), que no se decodifican igual en todos los contextos.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### Roles tipográficos por tarea
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 5. Espaciado
|
|
203
|
+
|
|
204
|
+
Escala base 4px — tokens `--space-1…24` (número = px/4): `4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96`
|
|
205
|
+
|
|
206
|
+
Aliases de uso (consumir estos, no números sueltos):
|
|
207
|
+
|
|
208
|
+
| Alias | Valor | Uso |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `--card-padding` | 24px (`--space-6`) | Padding de tarjetas (`--card-padding-mobile: 16px`) |
|
|
211
|
+
| `--section-gap` | 64px (`--space-16`) | Separación entre secciones (48–64px) |
|
|
212
|
+
| `--form-gap` | 20px (`--space-5`) | Gap entre campos de formulario |
|
|
213
|
+
| `--button-padding-md` | 12px 20px | Botón md (`--button-padding-sm: 8px 14px`) |
|
|
214
|
+
| `--control-height-sm/md/lg` | 36 / 44 / 52px | Alturas de controles |
|
|
215
|
+
| `--gutter` | 24px | Gutter del grid (`--gutter-mobile: 16px`) |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Ritmo entre textos, controles y tarjetas
|
|
220
|
+
|
|
221
|
+
| Relación | Separación | Token |
|
|
222
|
+
|---|---:|---|
|
|
223
|
+
| Etiqueta → control; control → ayuda/error | 6px | `--field-gap` |
|
|
224
|
+
| Título → descripción del mismo grupo | 8px | `--heading-gap` |
|
|
225
|
+
| Párrafos o bloques relacionados | 16px | `--content-gap` |
|
|
226
|
+
| Bloques directos dentro de una tarjeta | 16px | `--card-content-gap` |
|
|
227
|
+
| Campos consecutivos y pie del formulario | 20px | `--form-gap` |
|
|
228
|
+
| Tarjetas consecutivas | 32px | `--card-group-gap` |
|
|
229
|
+
| Secciones independientes | 64px | `--section-gap` |
|
|
230
|
+
|
|
231
|
+
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.
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
```html
|
|
236
|
+
<div class="stack stack--cards">
|
|
237
|
+
<section class="card" data-mode="operate">
|
|
238
|
+
<header class="text-group"><h2>Datos de contacto</h2><p>Usaremos estos datos para responder.</p></header>
|
|
239
|
+
<form class="form-stack">
|
|
240
|
+
<div class="field"><label class="field__label" for="nombre">Nombre</label><input class="input" id="nombre" autocomplete="name"></div>
|
|
241
|
+
<div class="field"><label class="field__label" for="correo">Correo</label><input class="input" id="correo" type="email" autocomplete="email"></div>
|
|
242
|
+
<div class="cluster"><button class="btn btn--primary" type="submit">Guardar</button><button class="btn btn--secondary" type="button">Cancelar</button></div>
|
|
243
|
+
</form>
|
|
244
|
+
</section>
|
|
245
|
+
<section class="card"><h2 class="ui-title">Otra sección</h2><p>El contenedor mantiene su distancia respecto a la tarjeta anterior.</p></section>
|
|
246
|
+
</div>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 6. Bordes, radios y focus
|
|
254
|
+
|
|
255
|
+
| Token | Valor | Uso |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `radius-sm` | 8px | Inputs, chips, badges |
|
|
258
|
+
| `radius-md` | 12px | Botones, dropdowns, menús |
|
|
259
|
+
| `radius-lg` | 16px | Tarjetas, modales, accordion |
|
|
260
|
+
| `radius-xl` | 24px | Hero, imágenes destacadas |
|
|
261
|
+
| `radius-full` | 9999px | Avatares, pills, toggles |
|
|
262
|
+
|
|
263
|
+
Los radios son amplios a propósito: las esquinas suaves bajan la tensión visual sin volver la UI infantil.
|
|
264
|
+
|
|
265
|
+
- Borde por defecto: `var(--border-width, 1px) solid var(--border)`.
|
|
266
|
+
- **Focus ring global de teclado** (en el framework, especificidad cero vía `:where()` — funciona aunque no uses el reset):
|
|
267
|
+
|
|
268
|
+
```css
|
|
269
|
+
:where(a, button, [role="button"], input, select, textarea, summary, [tabindex]):focus-visible {
|
|
270
|
+
outline: 2px solid var(--focus-color);
|
|
271
|
+
outline-offset: 2px;
|
|
272
|
+
border-radius: 2px;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Solo lo dispara el teclado (`:focus-visible`), nunca el click de ratón. `--focus-color` es un token del framework que vale `var(--accent)` en los dos modos, así que el contorno sigue al acento: `#4f6b58` en claro (5.62:1 sobre `--surface`) y `#9fc2a8` en oscuro (8.20:1). El SegmentedControl y el chip de métrica usan el mismo token para su foco.
|
|
277
|
+
|
|
278
|
+
**Transparencia y blur:** únicos usos permitidos — overlay de modal `rgba(44,54,48,.5)` y ring de focus. **No hay glassmorphism ni blur en Salvia Serena**; las superficies son opacas y planas.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 7. Elevación
|
|
283
|
+
|
|
284
|
+
Sombras suaves teñidas con la tinta verde `#2c3630`, nunca negro puro. Máximo **3 niveles visibles por pantalla**. **Sin resplandor de acento (glow):** la elevación es sombra y borde, nunca brillo.
|
|
285
|
+
|
|
286
|
+
### Niveles
|
|
287
|
+
|
|
288
|
+
| Nivel | Superficie | Sombra | Uso |
|
|
289
|
+
|---|---|---|---|
|
|
290
|
+
| 0 | `--bg`, sin sombra, borde discontinuo | — | Reposo del lienzo |
|
|
291
|
+
| 1 | `--surface` + borde `--border` | `--shadow-xs` / `--shadow-sm` | Inputs, tarjetas en reposo |
|
|
292
|
+
| 2 | `--surface` + borde `--border` | `--shadow-md` | Hover, dropdowns |
|
|
293
|
+
| 3 | `--surface` + borde `--border-strong` | `--shadow-lg` / `--shadow-xl` | Modales, popovers, diálogos críticos |
|
|
294
|
+
|
|
295
|
+
### Sombras
|
|
296
|
+
|
|
297
|
+
| Token | Valor | Uso |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| `shadow-xs` | `0 1px 2px rgba(44,54,48,.06)` | Inputs, indicador de SegmentedControl |
|
|
300
|
+
| `shadow-sm` | `0 1px 3px rgba(44,54,48,.08), 0 1px 2px rgba(44,54,48,.05)` | Tarjetas en reposo |
|
|
301
|
+
| `shadow-md` | `0 4px 8px rgba(44,54,48,.09), 0 2px 4px rgba(44,54,48,.05)` | Hover, dropdowns |
|
|
302
|
+
| `shadow-lg` | `0 12px 24px rgba(44,54,48,.12), 0 4px 8px rgba(44,54,48,.06)` | Modales, popovers, menús |
|
|
303
|
+
| `shadow-xl` | `0 24px 48px rgba(44,54,48,.16)` | Diálogos críticos |
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 8. Iconografía
|
|
308
|
+
|
|
309
|
+
- **Set:** [Lucide](https://lucide.dev) como sustituto *(flagged: no hay set propio de marca — si existe, reemplazar)*. Trazo 1.5–2px, esquinas redondeadas — casa con las formas suaves y el ritmo tranquilo de la marca.
|
|
310
|
+
- **Componente `Icon`:** inserta el SVG **inline** para heredar `currentColor` (un componente sirve para cualquier tono) y sobrevivir impresión/exportación. Acepta nombres kebab-case de Lucide; los nombres clásicos pre-renombre (`home`, `alert-circle`, `check-circle`, `alert-triangle`, `more-horizontal`…) se traducen automáticamente.
|
|
311
|
+
- **Tamaños:** 14px inline con texto pequeño · 16–18px inputs/chevrons · 20px botones/toolbars · 40px empty states (en `primary-300` decorativo).
|
|
312
|
+
- **El ✓ es del éxito:** `check` / `circle-check` acompañan siempre a un estado de éxito, en el tono `--success-*`.
|
|
313
|
+
- **Iconos botánicos** (`leaf`, `sprout`…) solo cuando representan un dato real —una categoría «verduras», por ejemplo—, nunca como adorno de marca.
|
|
314
|
+
- **Alineación óptica:** los iconos asimétricos (play, flechas, chevrons) se centran a ojo, no geométricamente — el centro matemático engaña con formas que pesan hacia un lado. Ajustar 1–2px a mano hasta que se vea centrado.
|
|
315
|
+
- Sin emoji; sin Unicode-como-icono (excepto el `/` tipográfico de breadcrumbs).
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 9. Marca / Wordmark
|
|
320
|
+
|
|
321
|
+
No hay logo ni imágenes de marca — el wordmark es puramente tipográfico. Donde un mockup necesite logotipo, se renderiza **"Higinio"** (o el nombre del producto) en `--font-display` — nunca se dibuja un logo aproximado.
|
|
322
|
+
|
|
323
|
+
- **"Higinio"** en Lora peso 600 (`--weight-semibold`), tracking `--tracking-display` (`-0.02em`), color `--text-primary` (tinta verde). Tamaño de referencia: 31px (`--text-3xl`). El 600 deja ver el trazo caligráfico de Lora sin endurecerlo; el 700 queda para titulares grandes. El `.navbar__brand` del framework usa el mismo peso (`--weight-semibold`) a `text-lg`.
|
|
324
|
+
- **Variante con descriptor:** debajo del nombre, el producto o el lema en Lora 600 itálica, `text-lg`, `--text-secondary`. Dentro de la marca es el único lugar con itálica; en el contenido, §4.1 la admite además en citas y subtítulos de portada.
|
|
325
|
+
- **Variante badge:** mismo texto/tamaño dentro de caja con fondo `--surface`, borde `1px --border`, radius `md` (12px), `shadow-sm`, padding `14px 22px`.
|
|
326
|
+
- El wordmark es texto, no un icono: Lora 600–700, nunca en UI ni párrafos.
|
|
327
|
+
- **Sentence case siempre:** "Higinio", nunca "HIGINIO" ni "higinio".
|
|
328
|
+
- Color `--text-primary`; `primary-300` jamás como texto, tampoco en el wordmark.
|
|
329
|
+
- Sin hojas, ramas ni isotipo botánico junto al nombre. No se genera ilustración para no inventar assets: usar placeholders y pedir los materiales reales.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## 10. Biblioteca de componentes
|
|
334
|
+
|
|
335
|
+
### 10.1 Botones
|
|
336
|
+
|
|
337
|
+
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`.
|
|
338
|
+
|
|
339
|
+
| Variante | Reposo | Hover | Active | Disabled |
|
|
340
|
+
|---|---|---|---|---|
|
|
341
|
+
| Primario | fondo `--action-bg`, texto `--text-on-action` | `--action-hover` | `--action-active`, sin sombra | fondo `--border`, texto `--text-disabled` |
|
|
342
|
+
| Secundario | fondo `--surface`, borde `--border-strong`, texto `--text-primary` | fondo `--bg` | fondo `--surface-alt` | opacidad 50% |
|
|
343
|
+
| Outline | fondo transparente, borde `--accent`, **texto `--link`** | fondo `--accent-soft`, borde `--accent-hover` | fondo `--accent-soft-strong` | opacidad 50% |
|
|
344
|
+
| Ghost | **texto `--link`** (`primary-600`) | fondo `--accent-soft` | fondo `--accent-soft-strong` | fondo `--border`, texto `--text-disabled` |
|
|
345
|
+
| Destructivo | fondo `danger-500`, texto `--text-on-danger` | oscurecer 8% | oscurecer 14% | opacidad 50% |
|
|
346
|
+
|
|
347
|
+
- `--text-on-danger` es un token del framework: `neutral-0` en claro (4.74:1 sobre `danger-500`) y la tinta oscura `#151a17` en oscuro (6.09:1), donde el rojo arcilla se aclara y el texto claro ya no alcanza AA. Los oscurecidos de hover y active (`color-mix` con negro al 92% y 86%) se mantienen por encima de 4.5:1 en los dos modos.
|
|
348
|
+
- Con icono: gap 8px, icono 20px.
|
|
349
|
+
- Loading: spinner 16px reemplaza el texto (el botón conserva su ancho — fijar `min-width`).
|
|
350
|
+
- Confirmar es una acción y va en salvia; el éxito es su resultado y se muestra después, en el tono de éxito con ✓. Ningún botón se pinta de `--success-500`.
|
|
351
|
+
|
|
352
|
+
### 10.2 IconButton
|
|
353
|
+
|
|
354
|
+
Botón cuadrado solo-icono para toolbars, acciones de fila y chrome. **Requiere `label` accesible** (`aria-label` + `title`). Radius `md`.
|
|
355
|
+
|
|
356
|
+
- Tamaños: sm 32px (icono 16) · md 44px, `--control-height-md` (icono 18) · lg 48px (icono 20). md y lg cumplen el objetivo táctil de 44×44px (§12); sm queda para chrome denso de escritorio, nunca como único destino táctil, así que el cierre de Toast, Modal y Drawer usa md.
|
|
357
|
+
- Variantes espejo de Button: `primary` (fondo `--accent`, icono `--text-on-primary`; hover `--accent-hover`), `secondary` (fondo `--surface`, borde `--border-strong`; hover `--bg`), `ghost` (transparente, icono `--text-secondary`; hover fondo `--surface-alt`) — **default `ghost`** porque vive en chrome denso, no como CTA.
|
|
358
|
+
- Disabled: opacidad 0.5. Transición de fondo 150ms.
|
|
359
|
+
|
|
360
|
+
### 10.3 Spinner
|
|
361
|
+
|
|
362
|
+
Export propio (extraído del estado loading de Button). SVG 16px default: círculo al 25% de opacidad + arco de ¼ sólido, stroke 2.5, rotación 0.8s linear infinita. Color default `--accent`; `--text-on-primary` dentro de botones primary/destructive.
|
|
363
|
+
|
|
364
|
+
### 10.4 Input
|
|
365
|
+
|
|
366
|
+
- Fondo `--surface`, borde `1px --border-strong`, radius `sm`, altura `--control-height-md`, padding `0 14px`, `text-base`.
|
|
367
|
+
- **Label:** `text-sm`, peso 500, `--text-secondary`, separado 6px del control. Siempre visible (no usar placeholder como label).
|
|
368
|
+
- **Placeholder:** `--text-disabled`.
|
|
369
|
+
- **Hover:** borde `--border-hover`, un paso más oscuro que el reposo: `neutral-500` en claro (5.65:1 sobre `--surface`) y `#a6b0a8` en oscuro (7.16:1). Lo comparten Textarea, Select, Combobox, Search y DatePicker.
|
|
370
|
+
- **Focus:** borde `--accent` + `--ring`. Transición 150ms.
|
|
371
|
+
- **Error:** borde `danger-500` + `--ring-danger` (ring arcilla suave); mensaje `text-sm` `--danger-text` con icono, 6px debajo (el borde y el ring siguen en `danger-500`, que como texto no llega a 4.5:1 sobre `--bg`).
|
|
372
|
+
- **Éxito (validación):** borde `success-500` + icono ✓ dentro del input.
|
|
373
|
+
- **Disabled:** fondo `--surface-alt`, texto `--text-disabled`, cursor `not-allowed`.
|
|
374
|
+
- **Texto de ayuda:** `text-sm` `--text-muted` debajo del input.
|
|
375
|
+
- Con icono leading: padding-left 40px, icono 18px `--text-disabled`.
|
|
376
|
+
- **`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.
|
|
377
|
+
- **Nunca bloquear el pegado** (`onpaste`): rompe gestores de contraseñas y códigos de verificación.
|
|
378
|
+
- **Al enviar con errores,** mover el foco al primer campo inválido (no solo pintarlo de rojo).
|
|
379
|
+
- **Campo obligatorio (`.field--required`):** el `.field__label` muestra un asterisco decorativo vía `::after` en color `--danger-text`, 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.
|
|
380
|
+
|
|
381
|
+
### 10.5 Textarea
|
|
382
|
+
|
|
383
|
+
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.
|
|
384
|
+
|
|
385
|
+
### 10.6 Select
|
|
386
|
+
|
|
387
|
+
- Trigger: igual que input + chevron 16px `--text-muted` (rota 180° al abrir, 220ms).
|
|
388
|
+
- Menú: fondo `--surface`, radius `md`, `shadow-lg`, padding 6px, max-height 320px con scroll.
|
|
389
|
+
- Opción: padding `10px 12px`, radius `sm`, hover fondo `--bg`, seleccionada fondo `--accent-soft` + texto `--link` + check.
|
|
390
|
+
- Entrada del menú: fade + translateY(-4px→0), 220ms (`--duration-base`).
|
|
391
|
+
|
|
392
|
+
### 10.7 SegmentedControl
|
|
393
|
+
|
|
394
|
+
Pill compacta de **2–4 opciones mutuamente excluyentes** (switches de vista, filtros rápidos). Para >4 opciones o labels largos, usar `Select`.
|
|
395
|
+
|
|
396
|
+
- Contenedor: fondo `--surface-alt`, radius `md`, padding 3px. Altura 40px (md) / 32px (sm); segmentos = altura − 6px, padding horizontal 16px.
|
|
397
|
+
- Opción activa: fondo `--surface`, radius `calc(radius-md - 3px)`, `shadow-xs`; **transición de fondo y color 220ms `--ease-out`** (swap de color, nunca animando `left`/`width`).
|
|
398
|
+
- Segmento activo: `--text-primary` semibold; inactivo: `--text-secondary` medium; transición de color 150ms. Texto `text-sm`.
|
|
399
|
+
|
|
400
|
+
### 10.8 Slider
|
|
401
|
+
|
|
402
|
+
Rango de valor único.
|
|
403
|
+
|
|
404
|
+
- Track: 6px de alto, radius full; porción rellena `--accent`, resto `--border` (gradiente vivo, sin elementos extra).
|
|
405
|
+
- Thumb: círculo `--surface` 18px, borde `2px --accent`, `shadow-sm`; **al presionar escala 1.15** (100ms).
|
|
406
|
+
- Label opcional `text-sm --text-secondary` arriba-izquierda; valor en `--font-mono --text-muted` arriba-derecha (`showValue`).
|
|
407
|
+
- Disabled: opacidad 0.5. Props: `min/max/step/value/onChange`.
|
|
408
|
+
|
|
409
|
+
### 10.9 Checkbox y Radio
|
|
410
|
+
|
|
411
|
+
- Tamaño 20px, fondo `--surface`, borde `2px --border-strong`, radius 4px (checkbox) / full (radio).
|
|
412
|
+
- Checked: fondo `--accent`, check `--text-on-primary` animado (stroke-dashoffset, 220ms). Marcar es seleccionar, no un estado de éxito.
|
|
413
|
+
- Hover: borde `primary-400`. Focus: ring como inputs.
|
|
414
|
+
- Label clicable, gap 10px, `text-base`. `RadioGroup` agrupa con `role="radiogroup"`.
|
|
415
|
+
- **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).
|
|
416
|
+
|
|
417
|
+
### 10.10 Switch
|
|
418
|
+
|
|
419
|
+
- Track 44×24px, radius full. Off: fondo `--border-strong`. On: fondo `--accent`.
|
|
420
|
+
- Thumb 20px `neutral-0` con `shadow-xs`; desliza 20px en 220ms `ease-spring`.
|
|
421
|
+
|
|
422
|
+
### 10.11 Badge y Tag
|
|
423
|
+
|
|
424
|
+
- Badge: `text-xs` peso 500, padding `2px 8px`, radius full. Neutro: fondo `--surface-alt` + texto `--text-secondary`. Semántico: fondo suave (`*-100`) + **texto `--*-text`** del mismo tono. El badge de éxito lleva su icono ✓ de 12px.
|
|
425
|
+
- Tag removible: fondo `--surface-alt`, borde `--border`, incluye botón × de 16px a la vista y 44px de área de toque (`::before` invisible centrado en la ×, como el cierre del Alert); hover del × fondo `--border`.
|
|
426
|
+
|
|
427
|
+
### 10.12 Avatar
|
|
428
|
+
|
|
429
|
+
- Imagen de usuario con fallback automático a iniciales (máx. 2, del nombre) sobre chip `--accent-soft-strong` con texto `--text-on-primary-solid` semibold (6.68:1). Si la imagen falla, cae a iniciales.
|
|
430
|
+
- Tamaños: sm 28px (fuente 12) · md 36px (14) · lg 48px (18) · xl 64px (24), o px directo. Radius full.
|
|
431
|
+
- Dot de presencia opcional: 28% del diámetro (mín. 8px), anillo `0 0 0 2px var(--surface)`: online `success-500` · busy `danger-500` · away `warning-500` · offline `neutral-300`.
|
|
432
|
+
|
|
433
|
+
### 10.13 Divider
|
|
434
|
+
|
|
435
|
+
- Regla hairline `1px --border`. `role="separator"`.
|
|
436
|
+
- Horizontal (default) · horizontal con label centrado (`text-xs` uppercase, `--tracking-label`, `--text-muted`, medium; líneas flexibles a los lados, gap 12px — patrón "o continúa con") · vertical (se estira al alto del padre flex).
|
|
437
|
+
|
|
438
|
+
### 10.14 Accordion
|
|
439
|
+
|
|
440
|
+
- Contenedor: borde `--border`, radius `lg`, fondo `--surface`, ítems separados por borde interno.
|
|
441
|
+
- Header: botón full-width, padding `16px 20px`, `text-base` medium `--text-primary`, hover fondo `--bg`; chevron 18px `--text-muted` rota 180° (220ms). `aria-expanded`.
|
|
442
|
+
- Panel: altura animada con `grid-template-rows` 0fr→1fr 280ms ease-out (con layout local controlado); contenido `text-sm --text-secondary`, padding `0 20px 16px`.
|
|
443
|
+
- Un panel abierto a la vez salvo `allowMultiple`; `defaultOpen` por ids.
|
|
444
|
+
|
|
445
|
+
### 10.15 Tooltip
|
|
446
|
+
|
|
447
|
+
Fondo `--tooltip-bg` (`neutral-900`), texto `--tooltip-text` (`neutral-0`, 14.80:1) `text-xs`, padding `6px 10px`, radius `sm`, flecha 6px. Delay de aparición 400ms, fade + translateY(4px→0) en 150ms. Máximo 1 línea de ayuda; si necesita más, usar popover.
|
|
448
|
+
|
|
449
|
+
**Skip-delay:** solo el primer tooltip del grupo espera los 400ms; mientras haya un tooltip visible, los hermanos aparecen al instante y sin animación de entrada. El usuario ya está explorando — repetirle el delay y el fade en cada icono estorba.
|
|
450
|
+
|
|
451
|
+
### 10.16 Alert
|
|
452
|
+
|
|
453
|
+
Banner de estado **inline y persistente** (vs Toast, transitorio): errores de formulario, avisos de plan.
|
|
454
|
+
|
|
455
|
+
- 4 tonos: fondo suave semántico (`*-100`), radius `md`, padding `14px 16px`.
|
|
456
|
+
- Icono semántico 20px en `*-500`: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`. En éxito el ✓ no es opcional.
|
|
457
|
+
- Título `text-sm` semibold y cuerpo `text-sm` en `--*-text`, a contraste completo (sin opacidad).
|
|
458
|
+
- Botón cerrar opcional (× 16px en el color del tono, caja visual de 24px) vía `onDismiss`; un `::before` invisible centrado lleva su área de toque a 44px (§12). `role="status"`.
|
|
459
|
+
|
|
460
|
+
### 10.17 Toast
|
|
461
|
+
|
|
462
|
+
- Posición: top-right (desktop), top-center (móvil). Ancho 360px. `ToastStack` los apila (gap 12px, máximo 3).
|
|
463
|
+
- Fondo `--surface`, borde izquierdo 3px semántico, `shadow-lg`, radius `md`, padding 14px 16px, icono semántico 20px (✓ en éxito).
|
|
464
|
+
- Entrada: slide-in desde la derecha + fade, 280ms. Salida: fade + translateX, 220ms.
|
|
465
|
+
- Auto-dismiss 5s (pausable con hover).
|
|
466
|
+
- Botón cerrar: IconButton ghost md (44px, §10.2).
|
|
467
|
+
- **Accesibilidad:** el contenedor lleva `role="status"` + `aria-live="polite"`; los toasts de error usan `role="alert"` para anunciarse de inmediato.
|
|
468
|
+
|
|
469
|
+
### 10.18 Progress y ProgressRing
|
|
470
|
+
|
|
471
|
+
Progreso **determinado** (para carga indeterminada usar `Skeleton`, no Progress).
|
|
472
|
+
|
|
473
|
+
- **Lineal:** track 8px radius full fondo `--surface-alt`; relleno en tono (`--accent`/`success-500`/`warning-500`/`danger-500`), width anima 280ms ease-out; label `text-sm --text-secondary` + porcentaje en `--font-mono --text-muted` opcionales. `role="progressbar"` + `aria-valuenow`.
|
|
474
|
+
- **ProgressRing:** SVG rotado -90°, track `--surface-alt`, arco en tono con `strokeLinecap: round`, `stroke-dashoffset` anima 280ms; tamaño default 56px, stroke 5; porcentaje centrado en mono (24% del tamaño).
|
|
475
|
+
- **Avance frente a meta:** mientras se avanza, el relleno es `--accent`; solo al cumplir la meta pasa a `success-500`, con ✓ y el texto que lo dice ("Meta cumplida").
|
|
476
|
+
|
|
477
|
+
### 10.19 Modal
|
|
478
|
+
|
|
479
|
+
- Overlay: `--overlay` (`rgba(44,54,48,.5)`), fade 220ms.
|
|
480
|
+
- Panel: fondo `--surface`, radius `lg`, `shadow-xl`, max-width 480px (confirm) / 640px (formulario), padding 24px.
|
|
481
|
+
- Entrada: fade + scale(0.96→1), 280ms. Salida: la misma transición en sentido inverso (280ms, `--duration-slow`).
|
|
482
|
+
- Header con título `text-xl` + IconButton cerrar; footer con acciones alineadas a la derecha (primaria al extremo).
|
|
483
|
+
- Cerrar con Esc y click en overlay (excepto flujos destructivos: el overlay no cierra para evitar descartes accidentales; Esc y los botones sí).
|
|
484
|
+
- `overscroll-behavior: contain` en el panel scrolleable — al llegar al fondo, el scroll no se encadena a la página de atrás.
|
|
485
|
+
|
|
486
|
+
**Accesibilidad y foco (obligatorio):**
|
|
487
|
+
|
|
488
|
+
- **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.
|
|
489
|
+
- **Foco al abrir:** el foco entra al panel (`.modal__panel` con `tabindex="-1"`) o al primer control enfocable del modal.
|
|
490
|
+
- **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).
|
|
491
|
+
- **Retorno de foco:** al cerrar (Esc, botón ×, Cancelar o click en overlay), el foco vuelve al elemento que abrió el modal.
|
|
492
|
+
- El panel se marca con `tabindex="-1"` para poder recibir foco programático sin quedar en el orden de tabulación.
|
|
493
|
+
|
|
494
|
+
### 10.20 Menu
|
|
495
|
+
|
|
496
|
+
Lista de acciones desplegable anclada a un trigger (acciones de fila, botones "más", menú de cuenta).
|
|
497
|
+
|
|
498
|
+
- Panel: fondo `--surface`, borde `--border`, radius `md`, `shadow-lg`, padding 6px, min-width 190px, offset 6px bajo el trigger, `align` left/right.
|
|
499
|
+
- Entrada: fade + translateY(-4px→0) 160ms ease-out.
|
|
500
|
+
- Ítem: `role="menuitem"`, padding `9px 12px`, radius `sm`, `text-sm --text-primary`, icono opcional 16px `--text-muted`, gap 10px; hover fondo `--bg`.
|
|
501
|
+
- Ítem `danger`: texto e icono `--danger-text`, hover fondo `danger-100` (6.28:1).
|
|
502
|
+
- Separador: 1px `--border`, margen `6px 4px`. Cierra con click fuera, Esc o al seleccionar.
|
|
503
|
+
|
|
504
|
+
### 10.21 Tabla
|
|
505
|
+
|
|
506
|
+
- Header: fondo `--bg`, `text-xs` uppercase `--tracking-label`, `--text-muted`, borde inferior `--border`. Cada `<th>` lleva `scope="col"`.
|
|
507
|
+
- Filas: altura 52px, borde inferior `--surface-alt`, hover fondo `--bg`.
|
|
508
|
+
- Celdas numéricas alineadas a la derecha con `tabular-nums` en `--font-mono`.
|
|
509
|
+
- Fila seleccionada: fondo `--accent-soft`.
|
|
510
|
+
|
|
511
|
+
**Variantes (clases del framework):**
|
|
512
|
+
|
|
513
|
+
- **`.table--zebra`** — filas alternas (`tr:nth-child(even)`) con fondo `--surface-alt`; el hover sigue ganando.
|
|
514
|
+
- **`.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.
|
|
515
|
+
- **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.
|
|
516
|
+
- **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).
|
|
517
|
+
|
|
518
|
+
**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.
|
|
519
|
+
|
|
520
|
+
### 10.22 Stat
|
|
521
|
+
|
|
522
|
+
Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
|
|
523
|
+
|
|
524
|
+
- Label `text-sm --text-muted` con icono opcional 16px (gap 8px, margen inferior 8px).
|
|
525
|
+
- Valor grande en `--font-mono text-3xl` semibold `--text-primary` (tracking -0.01em, line-height 1).
|
|
526
|
+
- Delta opcional en baseline (gap 10px): `text-sm` semibold con flecha 14px (`trending-up`/`trending-down`/`minus`). **El delta indica tendencia, no éxito ni fracaso:** la flecha sigue al signo y el tono por defecto es neutro (`--text-secondary`). En métricas de bienestar subir no siempre es bueno, y teñir una bajada de rojo choca con la voz «sin culpa» (§2).
|
|
527
|
+
- `deltaTone` (clases `.stat__delta--up` / `.stat__delta--down`, en `--success-text` / `--danger-text`) solo cuando el producto decide qué sentido es bueno. Aun así no sustituye al ✓: un logro se comunica con el tono de éxito, su icono y texto.
|
|
528
|
+
|
|
529
|
+
#### Chip de métrica
|
|
530
|
+
|
|
531
|
+
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.
|
|
532
|
+
|
|
533
|
+
- **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
|
|
534
|
+
- **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va semibold en `--link` (7.00:1 sobre `--surface`) con `tabular-nums`.
|
|
535
|
+
- **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.
|
|
536
|
+
- **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 anillo propio (outline 2px `--focus-color`, offset 2px).
|
|
537
|
+
- **Meta cumplida:** el salvia del valor no comunica el logro. Si la métrica llega a su meta, el chip lo dice con un icono ✓ en `--success-500` (modificador `.metric-chip--met`) y con texto en su nombre accesible («Agua: 8 de 8 vasos, meta cumplida»).
|
|
538
|
+
|
|
539
|
+
```html
|
|
540
|
+
<div class="metric-chips">
|
|
541
|
+
<span class="metric-chip"><span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span></span>
|
|
542
|
+
<button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
|
|
543
|
+
<span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
|
|
544
|
+
</button>
|
|
545
|
+
</div>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**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.
|
|
549
|
+
|
|
550
|
+
### 10.23 Pagination
|
|
551
|
+
|
|
552
|
+
Navegador de páginas 1-indexado, bajo una `Table`.
|
|
553
|
+
|
|
554
|
+
- Flechas prev/next (chevron 16px) + lista de páginas que **trunca con elipsis a partir de 7 páginas** (patrones `1…n`, ventana `page±1`).
|
|
555
|
+
- Celdas: min-width 36px, altura 36px, radius `sm`, `text-sm` medium `--text-secondary`; hover fondo `--surface-alt`.
|
|
556
|
+
- Página activa: chip sólido `--accent` texto `--text-on-primary` semibold, `aria-current="page"`.
|
|
557
|
+
- Flechas deshabilitadas en extremos (`--text-disabled`); elipsis no clicable.
|
|
558
|
+
|
|
559
|
+
### 10.24 Navegación
|
|
560
|
+
|
|
561
|
+
- **Navbar:** altura 64px, fondo `--surface`, borde inferior `--border`. Link `text-sm` medium `--text-secondary`; activo: texto `--accent-active` + indicador inferior 2px `--accent`.
|
|
562
|
+
- **Sidebar:** ancho 260px, fondo `--bg`, borde derecho `--border`. Ítem: radius `md`, padding `10px 12px`, `text-sm` medium `--text-secondary`; hover fondo `--surface-alt`; activo: fondo `--accent-soft-strong`, texto `--link` (5.36:1), icono `--accent`.
|
|
563
|
+
- **Tabs:** borde inferior `--border`; tab activa con texto `--accent-active` e indicador 2px `--accent` que se desliza entre tabs (280ms).
|
|
564
|
+
- **Breadcrumbs:** `text-sm` `--text-muted`, separador `/` (tipográfico), último ítem `--text-primary`; hover de los enlaces en `--link`.
|
|
565
|
+
|
|
566
|
+
#### Sidebar contraíble
|
|
567
|
+
|
|
568
|
+
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.
|
|
569
|
+
|
|
570
|
+
- **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`.
|
|
571
|
+
- **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.
|
|
572
|
+
- **Ítem activo:** `.is-active` (fondo `--accent-soft-strong`, texto `--link`) más `aria-current="page"`.
|
|
573
|
+
- **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`.
|
|
574
|
+
- **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.
|
|
575
|
+
- **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).
|
|
576
|
+
- **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).
|
|
577
|
+
|
|
578
|
+
```html
|
|
579
|
+
<nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
|
|
580
|
+
<div class="sidebar__group" role="group" aria-labelledby="navHoy">
|
|
581
|
+
<span class="sidebar__group-label" id="navHoy">Hoy</span>
|
|
582
|
+
<a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
|
|
583
|
+
</div>
|
|
584
|
+
<button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
|
|
585
|
+
<svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
|
|
586
|
+
</button>
|
|
587
|
+
</nav>
|
|
588
|
+
|
|
589
|
+
<!-- Solo por debajo de 768px -->
|
|
590
|
+
<button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
|
|
591
|
+
aria-expanded="false" aria-controls="navDrawer">…menu…</button>
|
|
592
|
+
<div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**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.
|
|
596
|
+
|
|
597
|
+
### 10.25 Skeleton y EmptyState
|
|
598
|
+
|
|
599
|
+
- Skeleton: bloque plano `--surface-alt` que pulsa en opacidad (`@keyframes ss-pulse`, de 1 a 0.5 y vuelta, 1.36s por tramo = 4 × `--duration-page`, `linear`, loop alternado). Sin degradado: solo cambia la `opacity` del bloque entero. Con `prefers-reduced-motion`, el reset global (§11.5) lo deja quieto y opaco. Radius igual al contenido que reemplaza.
|
|
600
|
+
- Estado vacío (`EmptyState`): icono 40px en `primary-300` decorativo, título `text-lg`, descripción `text-sm` `--text-muted`, CTA claro. Un estado vacío siempre invita a actuar, sin presionar ("Añade la primera cuando quieras").
|
|
601
|
+
|
|
602
|
+
### 10.26 Card, Link y Code
|
|
603
|
+
|
|
604
|
+
- **Card:** fondo `--surface`, borde `1px --border`, radius `lg`, padding 24px (16px en móvil), `shadow-sm`.
|
|
605
|
+
- **Card interactiva:** cursor pointer; hover eleva — `translateY(-2px)` + `shadow-md`; transición de transform/sombra 220ms (`--duration-base`) ease-out. Sin glow: en light-first la elevación es sombra + desplazamiento, nunca brillo.
|
|
606
|
+
- **Link:** color `--link` (`primary-600`, AA 7.00:1 sobre `--surface`); subrayado animado 1px `currentColor` que crece de 0%→100% del ancho desde la izquierda (`background-size`, 220ms ease-out). Externo: `target="_blank" rel="noopener noreferrer"` + icono flecha 12px, gap 4px.
|
|
607
|
+
- **Code (inline):** Source Code Pro `0.9em`, texto `--text-primary`, fondo `--surface-alt`, borde `1px --border`, radius `sm`, padding `1px 6px`.
|
|
608
|
+
|
|
609
|
+
### 10.27 AvatarGroup
|
|
610
|
+
|
|
611
|
+
Stack de avatares solapados para representar participantes de un proyecto, miembros de un equipo o asignados de una tarea. Complementa al `Avatar` (§10.12).
|
|
612
|
+
|
|
613
|
+
- **`.avatar-group`** — contenedor `inline-flex`; cada `.avatar` (y el `.avatar-group__more`) se solapa con `margin-left: -8px`.
|
|
614
|
+
- **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.
|
|
615
|
+
- **`.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.
|
|
616
|
+
- Para más de ~5 avatares, corta la lista y muestra el resto como `+N` (evita filas larguísimas).
|
|
617
|
+
|
|
618
|
+
### 10.28 Combobox / Autocomplete y Búsqueda
|
|
619
|
+
|
|
620
|
+
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).
|
|
621
|
+
|
|
622
|
+
**Combobox (`.combobox`):**
|
|
623
|
+
|
|
624
|
+
- **`.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`).
|
|
625
|
+
- **`.combobox__list`** — dropdown `role="listbox"` con id; fondo `--surface`, borde `--border`, `shadow-lg`, radius `md`, max-height 264px con scroll. Entra con fade + translateY(-4px→0), `--duration-base` `--ease-out`.
|
|
626
|
+
- **`.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).
|
|
627
|
+
- **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.
|
|
628
|
+
|
|
629
|
+
**Búsqueda (`.search`):**
|
|
630
|
+
|
|
631
|
+
- **`.search__icon`** — lupa 18px a la izquierda, `aria-hidden` (decorativa).
|
|
632
|
+
- **`.search__input`** — input con padding lateral 40px para dejar sitio al icono y a la ×.
|
|
633
|
+
- **`.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. Mide 32px a la vista y 44px de área de toque (`::before` invisible, como el cierre del Alert).
|
|
634
|
+
|
|
635
|
+
### 10.29 Drawer / Sheet
|
|
636
|
+
|
|
637
|
+
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.
|
|
638
|
+
|
|
639
|
+
- **`.drawer`** — contenedor/estado a pantalla completa (`position: fixed`), oculto hasta `.is-open`.
|
|
640
|
+
- **`.drawer__overlay`** — capa `--overlay` que atenúa el fondo; cierra al hacer click.
|
|
641
|
+
- **`.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`.
|
|
642
|
+
- **Variantes:** `.drawer--right` (default), `.drawer--left`, `.drawer--bottom` (esta última full-width, con radius superior `lg`).
|
|
643
|
+
- **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.
|
|
644
|
+
- **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`.
|
|
645
|
+
|
|
646
|
+
### 10.30 Popover
|
|
647
|
+
|
|
648
|
+
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.
|
|
649
|
+
|
|
650
|
+
- **`.popover`** — contenedor relativo (`.is-open` lo abre).
|
|
651
|
+
- **`.popover__trigger`** — disparador con `aria-haspopup="dialog"` + `aria-expanded`.
|
|
652
|
+
- **`.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.
|
|
653
|
+
- **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.
|
|
654
|
+
|
|
655
|
+
### 10.31 ButtonGroup
|
|
656
|
+
|
|
657
|
+
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).
|
|
658
|
+
|
|
659
|
+
- **`.btn-group`** — contenedor `inline-flex` con `role="group"` + `aria-label`. Usa los `.btn`/`.btn--secondary` existentes dentro.
|
|
660
|
+
- **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.
|
|
661
|
+
|
|
662
|
+
### 10.32 DatePicker
|
|
663
|
+
|
|
664
|
+
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).
|
|
665
|
+
|
|
666
|
+
- **`.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`).
|
|
667
|
+
- **`.datepicker__calendar`** — panel flotante `role="dialog"` con `aria-label`, mismo patrón que `.popover__panel`. Variante `.datepicker--right` ancla a la derecha.
|
|
668
|
+
- **`.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.
|
|
669
|
+
- **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`.
|
|
670
|
+
- **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.
|
|
671
|
+
- **Motion:** el panel aparece con fade + `scale(0.96→1)`, `--duration-base` `--ease-out`, bajo el techo `--duration-page`. Solo `transform`/`opacity`.
|
|
672
|
+
|
|
673
|
+
### 10.33 FileUpload
|
|
674
|
+
|
|
675
|
+
Zona de arrastre (dropzone) + lista de archivos.
|
|
676
|
+
|
|
677
|
+
- **`.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.
|
|
678
|
+
- **`.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").
|
|
679
|
+
- **`.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).
|
|
680
|
+
- **`.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 a la vista y 44px de área de toque, con `aria-label` que nombra el archivo). Un item que sube incluye la barra `.progress` (§10.18) dentro de su meta.
|
|
681
|
+
- **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").
|
|
682
|
+
- **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.
|
|
683
|
+
|
|
684
|
+
### 10.34 Stepper / Wizard
|
|
685
|
+
|
|
686
|
+
Indicador de progreso multipaso.
|
|
687
|
+
|
|
688
|
+
- **`.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).
|
|
689
|
+
- **Estados:** `.step--complete` (marcador `--accent` con **`#i-check`** en `--text-on-primary`, 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**.
|
|
690
|
+
- **Progreso, no éxito:** un paso completo marca avance dentro de un flujo y va en salvia. El resultado del flujo completo ("Plan guardado") se confirma aparte, con el tono de éxito y su ✓.
|
|
691
|
+
- **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.
|
|
692
|
+
- 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.
|
|
693
|
+
|
|
694
|
+
### 10.35 Kbd
|
|
695
|
+
|
|
696
|
+
Tecla del teclado para atajos y ayuda contextual.
|
|
697
|
+
|
|
698
|
+
- **`.kbd`** (para `<kbd>`) — tecla con fondo `--surface`, 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.
|
|
699
|
+
- **Combos:** varias `.kbd` con el signo **"+" literal** entre teclas (envueltas en `.kbd-combo` para alinear el "+" en `--text-muted`). Ejemplos: `⌘ + K`, `Ctrl + S`, `Esc`.
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## 11. Animaciones y motion
|
|
704
|
+
|
|
705
|
+
### 11.0 Cuándo animar (frequency gate)
|
|
706
|
+
|
|
707
|
+
La frecuencia de una acción decide su intensidad de movimiento:
|
|
708
|
+
|
|
709
|
+
| Frecuencia de la acción | Motion |
|
|
710
|
+
|---|---|
|
|
711
|
+
| Rara / memorable (primer uso, meta cumplida) | Puede deleitar con calma: un fundido o un trazo, nunca confeti |
|
|
712
|
+
| Ocasional (abrir un panel, un modal) | Transición clara y pausada |
|
|
713
|
+
| Diaria / repetida (navegar, registrar, alternar) | Sutil y rápida, casi imperceptible |
|
|
714
|
+
| Muy frecuente (100+/día) o de teclado | Sin animación; el movimiento estorbaría |
|
|
715
|
+
|
|
716
|
+
Antes de animar algo, pregúntate: ¿de dónde vino, qué cambió, a dónde se fue? Si no hay respuesta, no lo animes.
|
|
717
|
+
|
|
718
|
+
### 11.1 Tokens
|
|
719
|
+
|
|
720
|
+
Las duraciones de entrada son algo más largas que en un sistema utilitario, porque un ritmo pausado relaja; el feedback táctil (`instant`, `fast`) sigue siendo inmediato.
|
|
721
|
+
|
|
722
|
+
```css
|
|
723
|
+
:root {
|
|
724
|
+
--duration-instant: 100ms; /* feedback táctil: press, toggle */
|
|
725
|
+
--duration-fast: 150ms; /* hover, focus, color */
|
|
726
|
+
--duration-base: 220ms; /* dropdowns, tooltips, switches */
|
|
727
|
+
--duration-slow: 280ms; /* modales, toasts, drawers */
|
|
728
|
+
--duration-page: 340ms; /* transiciones de página/vista (techo) */
|
|
729
|
+
|
|
730
|
+
--ease-out: cubic-bezier(0.2, 0, 0, 1); /* entradas (default) */
|
|
731
|
+
--ease-in: cubic-bezier(0.4, 0, 1, 1); /* salidas */
|
|
732
|
+
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* moverse de A a B en pantalla */
|
|
733
|
+
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); /* toggles, checks (rebote sutil) */
|
|
734
|
+
--stagger-step: 30ms; /* paso entre ítems de una lista con stagger */
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### 11.2 Principios de movimiento
|
|
739
|
+
|
|
740
|
+
1. **Entra rápido, sale más rápido** — La salida siempre es ~20% más corta que la entrada.
|
|
741
|
+
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.
|
|
742
|
+
3. **Distancias cortas** — Desplazamientos de entrada de 4–8px; nada "vuela" por la pantalla.
|
|
743
|
+
4. **Jerarquía temporal** — En listas, stagger de `--stagger-step` (30ms) por ítem (máximo 6 ítems con stagger).
|
|
744
|
+
5. **La animación explica** — Todo movimiento responde a: ¿de dónde vino, qué cambió, a dónde se fue?
|
|
745
|
+
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.
|
|
746
|
+
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, pulso del skeleton).
|
|
747
|
+
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.
|
|
748
|
+
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`.
|
|
749
|
+
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.
|
|
750
|
+
|
|
751
|
+
### 11.3 Micro-interacciones por componente
|
|
752
|
+
|
|
753
|
+
| Elemento | Trigger | Animación |
|
|
754
|
+
|---|---|---|
|
|
755
|
+
| Botón | hover | color de fondo 150ms + `shadow-sm` |
|
|
756
|
+
| Botón | press | `scale(0.98)` 100ms |
|
|
757
|
+
| Link | hover | color 150ms + subrayado que crece de 0 a 100% (220ms) |
|
|
758
|
+
| Tarjeta interactiva | hover | `translateY(-2px)` + `shadow-md`, 220ms |
|
|
759
|
+
| Input | focus | ring que aparece con fade 150ms |
|
|
760
|
+
| Checkbox | check | trazo del check dibujándose 220ms `ease-spring` |
|
|
761
|
+
| Toggle | cambio | thumb desliza 220ms `ease-spring` |
|
|
762
|
+
| SegmentedControl | cambio | la opción activa toma fondo `--surface` 220ms `--ease-out` |
|
|
763
|
+
| Slider | press | thumb escala 1.15, 100ms |
|
|
764
|
+
| Acordeón | abrir | `grid-template-rows` (0fr→1fr) 280ms + fade del contenido |
|
|
765
|
+
| Dropdown / Menu | abrir | fade + translateY(-4px→0) 160–220ms |
|
|
766
|
+
| Modal | abrir | overlay fade 220ms; panel fade + scale(0.96→1) 280ms |
|
|
767
|
+
| Toast | entrar | slide-in + fade 280ms `ease-out` |
|
|
768
|
+
| Icon swap (p. ej. copiar→check) | cambio | cross-fade: opacity + scale(0.9→1), 150ms; nunca blur |
|
|
769
|
+
| Progress | cambio | width / stroke-dashoffset 280ms `ease-out` |
|
|
770
|
+
| Página/vista | cambio | fade + translateY(8px→0) 340ms |
|
|
771
|
+
|
|
772
|
+
### 11.4 Loading
|
|
773
|
+
|
|
774
|
+
- Spinner: ver §10.3.
|
|
775
|
+
- Progress: ver §10.18.
|
|
776
|
+
- Skeleton: pulso de opacidad de 1.36s por tramo, en loop alternado (ver §10.25).
|
|
777
|
+
- Si la carga dura <300ms, **no mostrar spinner** (evita parpadeo).
|
|
778
|
+
|
|
779
|
+
### 11.5 Accesibilidad del movimiento
|
|
780
|
+
|
|
781
|
+
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.
|
|
782
|
+
|
|
783
|
+
```css
|
|
784
|
+
@media (prefers-reduced-motion: reduce) {
|
|
785
|
+
:root {
|
|
786
|
+
--duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
|
|
787
|
+
--duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
|
|
788
|
+
}
|
|
789
|
+
html { scroll-behavior: auto; }
|
|
790
|
+
.spinner, .skeleton, .skeleton::after { animation: none; }
|
|
791
|
+
.btn, .icon-btn { transform: none; transition: background-color 80ms linear, color 80ms linear; }
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
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.
|
|
796
|
+
|
|
797
|
+
### 11.6 Anti-patrones de motion
|
|
798
|
+
|
|
799
|
+
Tells de motion genérico (a menudo generado por IA). La tool `validate_motion` del MCP los detecta con estos umbrales:
|
|
800
|
+
|
|
801
|
+
| Anti-patrón | Umbral / detección | En su lugar |
|
|
802
|
+
|---|---|---|
|
|
803
|
+
| `transition: all` | cualquier uso | Enumera: `transition: transform 220ms, opacity 220ms` |
|
|
804
|
+
| `ease-in` en entradas | `ease-in` en una entrada de UI | `--ease-out`; reserva `ease-in` para salidas |
|
|
805
|
+
| Animar propiedades de layout | `width/height/top/left/right/bottom/margin/padding` | `transform`/`opacity`. Excepciones: `width` de Progress, `grid-template-rows` del Accordion |
|
|
806
|
+
| Blur como recurso | `filter`/`backdrop-filter: blur()` | Elimínalo; fondos planos de lino, sombras teñidas de tinta |
|
|
807
|
+
| Duración excesiva | > el techo del sistema (`--duration-page`, 340ms) fuera de un loop de carga | Recorta a ≤ `--duration-page` |
|
|
808
|
+
| `scale(0)` | arrancar desde escala 0 | `scale(0.9–0.98)` + `opacity: 0` |
|
|
809
|
+
| Rebote en utilitarios | spring/overshoot en dropdown, menú, modal, tooltip, popover, select | `--ease-out`; el spring sutil se reserva a check y toggle |
|
|
810
|
+
| Overshoot excesivo | curva con y1 > 1.6 | Rebote apenas perceptible (y1 ≤ ~1.4) |
|
|
811
|
+
| blur-everywhere | ≥3 elementos con blur | Rediseña sin blur |
|
|
812
|
+
| hover-scale-en-todo | ≥3 elementos que escalan en hover | Reserva el scale para 1–2 acciones clave |
|
|
813
|
+
| stagger-spam | ≥2 listas con stagger en una vista | Una lista con stagger por vista (máx. 6 ítems) |
|
|
814
|
+
| uniform-fade | ≥4 entradas idénticas | Varía duración/origen o justifícalo |
|
|
815
|
+
| pulsing | indicadores que laten en loop (pulse/ping/glow) | Estado estático claro; spinner o pulso del skeleton solo en carga real. Un latido en un indicador de estado sugiere urgencia, y aquí nada urge |
|
|
816
|
+
| motion-on-mount-for-static | contenido estático que se anima al montar sin razón — no lo detecta la tool; juicio manual | Nada: aplica el frequency gate (§11.0) — si no responde de dónde/qué/adónde, no se anima |
|
|
817
|
+
|
|
818
|
+
### 11.7 Motion gap
|
|
819
|
+
|
|
820
|
+
Un "motion gap" es UI que aparece o desaparece sin transición y debería tenerla. Zonas donde suele faltar:
|
|
821
|
+
|
|
822
|
+
- Render condicional (`{cond && <X/>}`) y ternarios que cambian de vista.
|
|
823
|
+
- Tabs, secciones colapsables y acordeones.
|
|
824
|
+
- Campos condicionales de formularios.
|
|
825
|
+
- Toasts y notificaciones.
|
|
826
|
+
- Estados de carga y de error.
|
|
827
|
+
- Paneles de settings / inspector.
|
|
828
|
+
|
|
829
|
+
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.
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## 12. Accesibilidad y contraste
|
|
834
|
+
|
|
835
|
+
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.
|
|
836
|
+
|
|
837
|
+
- `--text-primary` (`neutral-800`) sobre `--surface`: **11.99:1** ✅ AAA (sobre `--bg` 11.16 · sobre `--surface-alt` 10.28).
|
|
838
|
+
- `--text-secondary` (`neutral-600`): **7.80:1** sobre `--surface` · 7.27 sobre `--bg` · 6.69 sobre `--surface-alt` ✅ AA.
|
|
839
|
+
- `--text-muted` (`neutral-500`): **5.65:1** sobre `--surface` · 5.26 sobre `--bg` · 4.84 sobre `--surface-alt` ✅ AA.
|
|
840
|
+
- `--link` (`primary-600`) sobre `--surface`: **7.00:1** ✅ AA (sobre `--bg` 6.52 · sobre `--surface-alt` 6.01) — por eso links y ghost usan `--link`.
|
|
841
|
+
- Blanco (`--text-on-primary`) sobre `primary-500`: **5.87:1** ✅ AA para texto normal (hover 7.31 · active 9.12).
|
|
842
|
+
- `--accent` sobre `--surface`: **5.62:1** ✅ (mínimo de UI 3:1).
|
|
843
|
+
- `--border-strong` (`neutral-400`) sobre `--surface`: **3.27:1** ✅ (mínimo de UI 3:1; sobre `--bg` 3.05).
|
|
844
|
+
- `primary-300` (`#97b09e`) sobre `--surface`: **2.23:1** — **nunca como texto**, solo decorativo. `primary-400`: 3.35:1, solo iconos y UI.
|
|
845
|
+
- Tonos `--*-text` garantizan AA sobre los fondos suaves `*-100`: success **6.15:1** · warning **5.33:1** · danger **6.28:1** · info **5.95:1**.
|
|
846
|
+
- Sólidos `-500` sobre `--surface`: success 4.72 · warning 3.10 · danger 4.74 · info 4.43. El texto de estado usa siempre `--*-text`; los `-500` quedan para iconos, bordes y rellenos.
|
|
847
|
+
- `--danger-text` como texto de error (mensaje de campo, ítem destructivo del menú, asterisco de obligatorio): **6.88:1** sobre `--bg` · 7.39 sobre `--surface` · 6.28 sobre `danger-100`. `danger-500` daría 4.41 sobre `--bg` y 4.03 sobre `danger-100`.
|
|
848
|
+
- **Éxito frente a acento:** ΔE76 entre `--accent` y `--success-500` = **15.5** (mínimo 15 en el test). El éxito, además, siempre lleva ✓.
|
|
849
|
+
- Tokens de componente del framework (los fija el mismo test): contorno de foco `--focus-color` sobre `--surface` **5.62:1** · `--text-on-danger` sobre `danger-500` **4.74:1** (hover 5.42 · active 5.97) · `--border-hover` sobre `--surface` **5.65:1**.
|
|
850
|
+
- Otros pares de componentes: avatar (`--text-on-primary-solid` sobre `--accent-soft-strong`) 6.68 · sidebar activo (`--link` sobre `--accent-soft-strong`) 5.36 · tooltip (`neutral-0` sobre `neutral-900`) 14.80.
|
|
851
|
+
- Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y UI.
|
|
852
|
+
- Focus visible siempre (ring global `:focus-visible`, §6); estado nunca comunicado solo con color (icono + texto).
|
|
853
|
+
- Touch targets mínimos: 44×44px.
|
|
854
|
+
- IconButton siempre con `aria-label`; Progress con `role="progressbar"`; Alert con `role="status"`; Divider con `role="separator"`; Pagination con `aria-current="page"`.
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
## 13. Tokens CSS consolidados
|
|
859
|
+
|
|
860
|
+
```css
|
|
861
|
+
/* ============================================================
|
|
862
|
+
Salvia Serena — Design Tokens v1.0
|
|
863
|
+
Sistema de diseño de Higinio Sosa · light-first
|
|
864
|
+
Fuente de verdad: DESIGN-salvia-serena.md
|
|
865
|
+
Uso: importar ANTES de cualquier estilo propio.
|
|
866
|
+
Los componentes consumen SOLO aliases semánticos, nunca hex.
|
|
867
|
+
============================================================ */
|
|
868
|
+
|
|
869
|
+
@import url("https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,600;0,700;1,600&family=Nunito+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Source+Code+Pro:wght@400;500&display=swap");
|
|
870
|
+
|
|
871
|
+
:root {
|
|
872
|
+
/* == Primitivos: primario (Salvia) == */
|
|
873
|
+
--primary-50: #e2eadf; --primary-100: #d4e0d1; --primary-200: #b8cbb7;
|
|
874
|
+
--primary-300: #97b09e; --primary-400: #73907c; --primary-500: #4f6b58;
|
|
875
|
+
--primary-600: #435c4b; --primary-700: #384d3f; --primary-800: #304236;
|
|
876
|
+
--primary-900: #2c3630;
|
|
877
|
+
|
|
878
|
+
/* == Primitivos: neutrales (Lino) == */
|
|
879
|
+
--neutral-0: #fbfaf6; --neutral-50: #f3f2ec; --neutral-100: #eae9e1;
|
|
880
|
+
--neutral-200: #dfe0d6; --neutral-300: #b9beb4; --neutral-400: #858d84;
|
|
881
|
+
--neutral-500: #5c675f; --neutral-600: #47524b; --neutral-700: #38423b;
|
|
882
|
+
--neutral-800: #2c3630; --neutral-900: #1f2622;
|
|
883
|
+
|
|
884
|
+
/* == Primitivos: semánticos (ramp de 3: sólido / fondo / texto AA) ==
|
|
885
|
+
El éxito es verde azulado, lejos del acento salvia: el acento nunca comunica éxito. */
|
|
886
|
+
--success-500: #2f7d6a; --success-100: #dcefe8; --success-text: #22604f;
|
|
887
|
+
--warning-500: #b8862f; --warning-100: #f6ecd8; --warning-text: #7e5a17;
|
|
888
|
+
--danger-500: #b5523f; --danger-100: #f6e4df; --danger-text: #8a3a2b;
|
|
889
|
+
--info-500: #5b7891; --info-100: #e3eaf0; --info-text: #3f5a70;
|
|
890
|
+
|
|
891
|
+
/* == Aliases semánticos (los componentes consumen SOLO esto) == */
|
|
892
|
+
--bg: var(--neutral-50);
|
|
893
|
+
--surface: var(--neutral-0);
|
|
894
|
+
--surface-alt: var(--neutral-100);
|
|
895
|
+
--border: var(--neutral-200);
|
|
896
|
+
--border-strong: var(--neutral-400);
|
|
897
|
+
--text-primary: var(--neutral-800);
|
|
898
|
+
--text-secondary: var(--neutral-600);
|
|
899
|
+
--text-muted: var(--neutral-500);
|
|
900
|
+
--text-disabled: var(--neutral-400);
|
|
901
|
+
--text-on-primary: #ffffff;
|
|
902
|
+
--text-on-primary-solid: var(--primary-700);
|
|
903
|
+
--accent: var(--primary-500);
|
|
904
|
+
--accent-hover: var(--primary-600);
|
|
905
|
+
--accent-active: var(--primary-700);
|
|
906
|
+
--accent-soft: var(--primary-50);
|
|
907
|
+
--accent-soft-strong: var(--primary-100);
|
|
908
|
+
--link: var(--primary-600); /* AA 7.00:1 sobre --surface — texto de links y ghost */
|
|
909
|
+
|
|
910
|
+
/* == Tipografía == */
|
|
911
|
+
--font-display: "Lora", Georgia, serif;
|
|
912
|
+
--font-body: "Nunito Sans", system-ui, -apple-system, sans-serif;
|
|
913
|
+
--font-mono: "Source Code Pro", ui-monospace, monospace;
|
|
914
|
+
--text-xs: .75rem; --text-sm: .875rem; --text-base: 1rem; --text-lg: 1.125rem;
|
|
915
|
+
--text-xl: 1.25rem; --text-2xl: 1.5625rem; --text-3xl: 1.9375rem;
|
|
916
|
+
--text-4xl: 2.4375rem; --text-5xl: 3.0625rem;
|
|
917
|
+
--leading-xs: 1.5; --leading-sm: 1.5; --leading-base: 1.6; --leading-lg: 1.55;
|
|
918
|
+
--leading-xl: 1.4; --leading-2xl: 1.3; --leading-3xl: 1.25;
|
|
919
|
+
--leading-4xl: 1.15; --leading-5xl: 1.1;
|
|
920
|
+
--tracking-display: -0.02em; --tracking-body: 0; --tracking-label: 0.04em;
|
|
921
|
+
--weight-regular: 400; --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700;
|
|
922
|
+
--measure: 65ch;
|
|
923
|
+
|
|
924
|
+
/* == Espaciado (escala base 4px) == */
|
|
925
|
+
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
|
|
926
|
+
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
|
|
927
|
+
--space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
|
|
928
|
+
--card-padding: var(--space-6); --card-padding-mobile: var(--space-4);
|
|
929
|
+
--section-gap: var(--space-16); --form-gap: var(--space-5);
|
|
930
|
+
--button-padding-md: 12px 20px; --button-padding-sm: 8px 14px;
|
|
931
|
+
--control-height-sm: 36px; --control-height-md: 44px; --control-height-lg: 52px;
|
|
932
|
+
--gutter: 24px; --gutter-mobile: 16px;
|
|
933
|
+
|
|
934
|
+
/* == Radios, bordes y sombras (teñidas de tinta verde, nunca negro puro) == */
|
|
935
|
+
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
|
|
936
|
+
--radius-xl: 24px; --radius-full: 9999px;
|
|
937
|
+
--border-width: 1px;
|
|
938
|
+
--shadow-xs: 0 1px 2px rgba(44,54,48,.06);
|
|
939
|
+
--shadow-sm: 0 1px 3px rgba(44,54,48,.08), 0 1px 2px rgba(44,54,48,.05);
|
|
940
|
+
--shadow-md: 0 4px 8px rgba(44,54,48,.09), 0 2px 4px rgba(44,54,48,.05);
|
|
941
|
+
--shadow-lg: 0 12px 24px rgba(44,54,48,.12), 0 4px 8px rgba(44,54,48,.06);
|
|
942
|
+
--shadow-xl: 0 24px 48px rgba(44,54,48,.16);
|
|
943
|
+
|
|
944
|
+
/* == Focus == */
|
|
945
|
+
--ring: 0 0 0 3px rgba(79,107,88,.25);
|
|
946
|
+
--ring-color: rgba(79,107,88,.25);
|
|
947
|
+
|
|
948
|
+
/* == Motion (pausado: nada urge) == */
|
|
949
|
+
--duration-instant: 100ms; --duration-fast: 150ms; --duration-base: 220ms;
|
|
950
|
+
--duration-slow: 280ms; --duration-page: 340ms;
|
|
951
|
+
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
|
952
|
+
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
953
|
+
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* moverse de A a B en pantalla */
|
|
954
|
+
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
|
|
955
|
+
--stagger-step: 30ms;
|
|
956
|
+
|
|
957
|
+
color-scheme: light;
|
|
958
|
+
|
|
959
|
+
--overlay: rgba(44, 54, 48, .5);
|
|
960
|
+
--tooltip-bg: var(--neutral-900);
|
|
961
|
+
--tooltip-text: var(--neutral-0);
|
|
962
|
+
--focus-color: var(--accent);
|
|
963
|
+
--text-on-danger: var(--neutral-0);
|
|
964
|
+
--border-hover: var(--neutral-500);
|
|
965
|
+
--ring-danger: 0 0 0 3px rgba(181,82,63,.22);
|
|
966
|
+
|
|
967
|
+
--action-bg: var(--accent);
|
|
968
|
+
--action-hover: var(--accent-hover);
|
|
969
|
+
--action-active: var(--accent-active);
|
|
970
|
+
--text-on-action: var(--text-on-primary);
|
|
971
|
+
--control-border: var(--border-strong);
|
|
972
|
+
--placeholder: var(--text-muted);
|
|
973
|
+
--selection-bg: var(--action-bg);
|
|
974
|
+
--selection-text: var(--text-on-action);
|
|
975
|
+
--font-ui-heading: var(--font-body);
|
|
976
|
+
--text-ui-title: 2rem;
|
|
977
|
+
--touch-target: 44px;
|
|
978
|
+
--text-display-fluid: clamp(2.5rem, 1.8rem + 2.4vw, 4.5rem);
|
|
979
|
+
|
|
980
|
+
/* Series categóricas: orden estable; acompañar con etiqueta o forma. */
|
|
981
|
+
--chart-1: #549864;
|
|
982
|
+
--chart-2: #8d4226;
|
|
983
|
+
--chart-3: #179a8e;
|
|
984
|
+
--chart-4: #81416d;
|
|
985
|
+
--chart-5: #8a8b39;
|
|
986
|
+
--chart-6: #195f91;
|
|
987
|
+
--chart-7: #996f18;
|
|
988
|
+
/* Ritmo de contenido y formularios. */
|
|
989
|
+
--field-gap: 6px;
|
|
990
|
+
--heading-gap: var(--space-2);
|
|
991
|
+
--content-gap: var(--space-4);
|
|
992
|
+
--card-content-gap: var(--space-4);
|
|
993
|
+
--card-group-gap: var(--space-8);
|
|
994
|
+
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* == Modo oscuro (variante) — alternar data-theme en <html> ==
|
|
998
|
+
Casi sin sombras: las capas se separan por superficie y borde, y solo
|
|
999
|
+
los overlays (lg/xl) conservan una sombra tenue. */
|
|
1000
|
+
[data-theme="dark"] {
|
|
1001
|
+
--bg: #151a17;
|
|
1002
|
+
--surface: #1d231f;
|
|
1003
|
+
--surface-alt: #252c27;
|
|
1004
|
+
--border: #2d3530;
|
|
1005
|
+
--border-strong: #6f7b72;
|
|
1006
|
+
--text-primary: #e3e8e2;
|
|
1007
|
+
--text-secondary: #c4ccc5;
|
|
1008
|
+
--text-muted: #a6b0a8;
|
|
1009
|
+
--text-disabled: #79837b;
|
|
1010
|
+
--accent: #9fc2a8;
|
|
1011
|
+
--accent-hover: #b5d2bc;
|
|
1012
|
+
--accent-active: #8ab295;
|
|
1013
|
+
--accent-soft: #26332b;
|
|
1014
|
+
--accent-soft-strong: #2f4035;
|
|
1015
|
+
--text-on-primary: #151a17;
|
|
1016
|
+
--text-on-primary-solid: #b5d2bc;
|
|
1017
|
+
--link: #b5d2bc;
|
|
1018
|
+
--success-500: #6cc3ac; --success-100: #14271f; --success-text: #8ad3bf;
|
|
1019
|
+
--warning-500: #d9a94f; --warning-100: #2a2213; --warning-text: #e6c17e;
|
|
1020
|
+
--danger-500: #d9806d; --danger-100: #2c1814; --danger-text: #e9a597;
|
|
1021
|
+
--info-500: #8fb0c9; --info-100: #18222b; --info-text: #a9c4d8;
|
|
1022
|
+
--ring: 0 0 0 3px rgba(159,194,168,.3);
|
|
1023
|
+
--ring-color: rgba(159,194,168,.3);
|
|
1024
|
+
--shadow-xs: none;
|
|
1025
|
+
--shadow-sm: none;
|
|
1026
|
+
--shadow-md: none;
|
|
1027
|
+
--shadow-lg: 0 8px 24px rgba(8,10,9,.3);
|
|
1028
|
+
--shadow-xl: 0 16px 32px rgba(8,10,9,.35);
|
|
1029
|
+
color-scheme: dark;
|
|
1030
|
+
|
|
1031
|
+
--overlay: rgba(0, 0, 0, .6);
|
|
1032
|
+
--tooltip-bg: #e3e8e2;
|
|
1033
|
+
--tooltip-text: #2c3630;
|
|
1034
|
+
--focus-color: var(--accent);
|
|
1035
|
+
--text-on-danger: #151a17;
|
|
1036
|
+
--border-hover: #a6b0a8;
|
|
1037
|
+
--ring-danger: 0 0 0 3px rgba(217,128,109,.25);
|
|
1038
|
+
|
|
1039
|
+
--selection-bg: var(--action-bg);
|
|
1040
|
+
--selection-text: var(--text-on-action);
|
|
1041
|
+
|
|
1042
|
+
/* Series categóricas: orden estable; acompañar con etiqueta o forma. */
|
|
1043
|
+
--chart-1: #60a46f;
|
|
1044
|
+
--chart-2: #a85a3f;
|
|
1045
|
+
--chart-3: #2da79b;
|
|
1046
|
+
--chart-4: #9b5986;
|
|
1047
|
+
--chart-5: #818230;
|
|
1048
|
+
--chart-6: #3678ac;
|
|
1049
|
+
--chart-7: #946b10;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/* == Accesibilidad del movimiento == */
|
|
1053
|
+
|
|
1054
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1055
|
+
:root {
|
|
1056
|
+
--duration-instant: 0ms; --duration-fast: 0ms; --duration-base: 0ms;
|
|
1057
|
+
--duration-slow: 0ms; --duration-page: 0ms; --stagger-step: 0ms;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Estilos base (extracto del framework — clase `.salvia-reset` aplicable a `body` o contenedor): fondo `--bg`, color `--text-primary`, `--font-body`/`--text-base`/`--leading-base`, antialiased; **h1 en display bold y h2 en display semibold**, con tracking display; **h3–h6 en body semibold**; `p { max-width: var(--measure) }`; `table { font-variant-numeric: tabular-nums }`; + focus ring global (§6).
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## 14. Breakpoints y grid
|
|
1067
|
+
|
|
1068
|
+
| Token | Ancho | Contenedor |
|
|
1069
|
+
|---|---|---|
|
|
1070
|
+
| `sm` | 640px | 100% - 32px |
|
|
1071
|
+
| `md` | 768px | 720px |
|
|
1072
|
+
| `lg` | 1024px | 960px |
|
|
1073
|
+
| `xl` | 1280px | 1200px |
|
|
1074
|
+
|
|
1075
|
+
Grid de 12 columnas, gutter 24px (16px móvil). Mobile-first. Navbar fija de 64px; sidebar de 260px cuando aplica (riel de 64px si es contraíble, §10.24).
|
|
1076
|
+
|
|
1077
|
+
**Touch y viewport:**
|
|
1078
|
+
|
|
1079
|
+
- `touch-action: manipulation` en botones y links — elimina el delay de double-tap en móvil.
|
|
1080
|
+
- Alturas de viewport con `100dvh`, no `100vh` (la barra de iOS cambia el alto visible).
|
|
1081
|
+
- **Receta de overflow:** `min-width: 0` en los hijos flex que deben truncar; texto largo con `text-overflow: ellipsis` o `line-clamp`, siempre con `title` o tooltip que exponga el contenido completo.
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
### Adaptación de los componentes
|
|
1086
|
+
|
|
1087
|
+
- Reflow a 320 CSS px sin recortar contenido ni ocultar el desbordamiento del documento. Revisa también texto al 200% y zoom al 400%.
|
|
1088
|
+
- 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.
|
|
1089
|
+
- 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.
|
|
1090
|
+
- 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.
|
|
1091
|
+
- 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.
|
|
1092
|
+
- 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.
|
|
1093
|
+
|
|
1094
|
+
---
|
|
1095
|
+
|
|
1096
|
+
## 15. Modo oscuro (variante)
|
|
1097
|
+
|
|
1098
|
+
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 — del lino al sol a un verde bosque al anochecer.
|
|
1099
|
+
|
|
1100
|
+
### 15.1 Filosofía de la inversión
|
|
1101
|
+
|
|
1102
|
+
- Los fondos se derivan de la tinta verde `#2c3630` (nunca negro puro ni gris genérico): `#151a17` hereda el matiz salvia de la marca.
|
|
1103
|
+
- El acento se **aclara**: sobre fondo oscuro, `#4f6b58` pierde fuerza, así que el rol de acento pasa a un salvia claro `#9fc2a8` y el texto sobre él se vuelve oscuro (`#151a17`). El éxito también se aclara (`#6cc3ac`) y conserva su distancia con el acento (ΔE76 16.2).
|
|
1104
|
+
- Casi sin sombras: la elevación se apoya en superficies más claras + bordes, y solo los overlays (`--shadow-lg`/`--shadow-xl`) conservan una sombra tenue.
|
|
1105
|
+
|
|
1106
|
+
### 15.2 Mapeo de tokens
|
|
1107
|
+
|
|
1108
|
+
| Token semántico | Light | Dark |
|
|
1109
|
+
|---|---|---|
|
|
1110
|
+
| `--bg` | `#f3f2ec` | `#151a17` |
|
|
1111
|
+
| `--surface` | `#fbfaf6` | `#1d231f` |
|
|
1112
|
+
| `--surface-alt` | `#eae9e1` | `#252c27` |
|
|
1113
|
+
| `--border` | `#dfe0d6` | `#2d3530` |
|
|
1114
|
+
| `--border-strong` | `#858d84` | `#6f7b72` |
|
|
1115
|
+
| `--text-primary` | `#2c3630` | `#e3e8e2` |
|
|
1116
|
+
| `--text-secondary` | `#47524b` | `#c4ccc5` |
|
|
1117
|
+
| `--text-muted` | `#5c675f` | `#a6b0a8` |
|
|
1118
|
+
| `--text-disabled` | `#858d84` | `#79837b` |
|
|
1119
|
+
| `--accent` (botones, focus) | `#4f6b58` | `#9fc2a8` |
|
|
1120
|
+
| `--accent-hover` | `#435c4b` | `#b5d2bc` |
|
|
1121
|
+
| `--accent-active` | `#384d3f` | `#8ab295` |
|
|
1122
|
+
| `--accent-soft` | `#e2eadf` | `#26332b` |
|
|
1123
|
+
| `--accent-soft-strong` | `#d4e0d1` | `#2f4035` |
|
|
1124
|
+
| `--text-on-primary` | `#ffffff` | `#151a17` |
|
|
1125
|
+
| `--text-on-primary-solid` | `#384d3f` | `#b5d2bc` |
|
|
1126
|
+
| `--link` | `#435c4b` | `#b5d2bc` |
|
|
1127
|
+
| `--success` | `#2f7d6a` | `#6cc3ac` |
|
|
1128
|
+
| `--warning` | `#b8862f` | `#d9a94f` |
|
|
1129
|
+
| `--danger` | `#b5523f` | `#d9806d` |
|
|
1130
|
+
| `--info` | `#5b7891` | `#8fb0c9` |
|
|
1131
|
+
| `--ring` | `rgba(79,107,88,.25)` | `rgba(159,194,168,.3)` |
|
|
1132
|
+
|
|
1133
|
+
Fondos suaves semánticos en dark: `success #14271f` · `warning #2a2213` · `danger #2c1814` · `info #18222b`.
|
|
1134
|
+
|
|
1135
|
+
### 15.3 Elevación en dark
|
|
1136
|
+
|
|
1137
|
+
| Nivel | Superficie | Sombra |
|
|
1138
|
+
|---|---|---|
|
|
1139
|
+
| 0 | `#151a17` | — |
|
|
1140
|
+
| 1 | `#1d231f` + borde `#2d3530` | ninguna (`--shadow-xs`/`--shadow-sm`: `none`) |
|
|
1141
|
+
| 2 | `#1d231f` + borde `#2d3530`; hover sobre `#252c27` | ninguna (`--shadow-md`: `none`) |
|
|
1142
|
+
| 3 | `#1d231f` + borde `#6f7b72` | `0 8px 24px rgba(8,10,9,.3)` (`lg`) · `0 16px 32px rgba(8,10,9,.35)` (`xl`) |
|
|
1143
|
+
|
|
1144
|
+
### 15.4 Ajustes de componentes en dark
|
|
1145
|
+
|
|
1146
|
+
- **Botón primario:** fondo `#9fc2a8`, texto `#151a17` (9.02:1 ✅). Hover `#b5d2bc` (10.83:1), active `#8ab295` (7.45:1).
|
|
1147
|
+
- **Inputs:** fondo `--surface` `#1d231f`, borde `#6f7b72` (3.62:1), hover `#a6b0a8` (`--border-hover`), placeholder `#79837b`; focus borde `#9fc2a8` + ring.
|
|
1148
|
+
- **Botón destructivo:** fondo `#d9806d`, texto `#151a17` (`--text-on-danger`, 6.09:1).
|
|
1149
|
+
- **Foco de teclado:** contorno `#9fc2a8` (`--focus-color`, 8.20:1 sobre `--surface`).
|
|
1150
|
+
- **Tabla:** header y hover de fila `#151a17` (`--bg`), seleccionada `#26332b` (`--accent-soft`).
|
|
1151
|
+
- **Sidebar activo:** fondo `#2f4035`, texto `#b5d2bc` (6.77:1), icono `#9fc2a8`.
|
|
1152
|
+
- **Tooltip:** se invierte — fondo `#e3e8e2`, texto `#2c3630` (10.08:1).
|
|
1153
|
+
- **Overlay de modal:** `rgba(0,0,0,.6)`.
|
|
1154
|
+
- **Card interactiva:** sin sombras intermedias, el hover se lee por el desplazamiento de 2px y el borde.
|
|
1155
|
+
- **Skeleton:** base `#252c27`, con el mismo pulso de opacidad que en claro.
|
|
1156
|
+
- Texto claro sobre oscuro "engorda": bajar un peso donde sea posible (500→400).
|
|
1157
|
+
|
|
1158
|
+
### 15.5 Contraste en dark
|
|
1159
|
+
|
|
1160
|
+
Ratios calculados, no estimados (mismo test que §12).
|
|
1161
|
+
|
|
1162
|
+
- `--text-primary` `#e3e8e2` sobre `#151a17`: **14.18:1** ✅ AAA (sobre `--surface` 12.88 · sobre `--surface-alt` 11.52).
|
|
1163
|
+
- `--text-secondary` `#c4ccc5`: 10.73 / 9.75 / 8.71 · `--text-muted` `#a6b0a8`: 7.88 / 7.16 / 6.40 (sobre `--bg` / `--surface` / `--surface-alt`) ✅.
|
|
1164
|
+
- `--link` `#b5d2bc` sobre `#151a17`: **10.83:1** ✅ (sobre `--surface` 9.84 · sobre `--surface-alt` 8.79).
|
|
1165
|
+
- `--text-on-primary` `#151a17` sobre `--accent` `#9fc2a8`: **9.02:1** ✅.
|
|
1166
|
+
- `--accent` sobre `--surface`: **8.20:1** · `--border-strong` sobre `--surface`: **3.62:1** ✅ (mínimo de UI 3:1).
|
|
1167
|
+
- Tokens de componente: `--focus-color` sobre `--surface` **8.20:1** · `--text-on-danger` sobre `danger-500` **6.09:1** (hover 5.23 · active 4.62) · `--border-hover` sobre `--surface` **7.16:1**.
|
|
1168
|
+
- Tonos `--*-text` sobre sus `*-100` en dark: success **9.07:1** · warning **9.20:1** · danger **8.25:1** · info **8.89:1**. `--danger-text` sobre `--bg`: **8.64:1** · sobre `--surface`: 7.84.
|
|
1169
|
+
- `--text-on-primary-solid` `#b5d2bc` sobre `--accent-soft-strong` `#2f4035` (iniciales del avatar, `+N` del AvatarGroup): **6.77:1**.
|
|
1170
|
+
- **Éxito frente a acento:** ΔE76 = **16.2** (mínimo 15).
|
|
1171
|
+
|
|
1172
|
+
### 15.6 Implementación
|
|
1173
|
+
|
|
1174
|
+
```css
|
|
1175
|
+
[data-theme="dark"] {
|
|
1176
|
+
--bg: #151a17;
|
|
1177
|
+
--surface: #1d231f;
|
|
1178
|
+
--surface-alt: #252c27;
|
|
1179
|
+
--border: #2d3530;
|
|
1180
|
+
--border-strong: #6f7b72;
|
|
1181
|
+
--text-primary: #e3e8e2;
|
|
1182
|
+
--text-secondary: #c4ccc5;
|
|
1183
|
+
--text-muted: #a6b0a8;
|
|
1184
|
+
--text-disabled: #79837b;
|
|
1185
|
+
--accent: #9fc2a8;
|
|
1186
|
+
--accent-hover: #b5d2bc;
|
|
1187
|
+
--accent-active: #8ab295;
|
|
1188
|
+
--accent-soft: #26332b;
|
|
1189
|
+
--accent-soft-strong: #2f4035;
|
|
1190
|
+
--text-on-primary: #151a17;
|
|
1191
|
+
--text-on-primary-solid: #b5d2bc;
|
|
1192
|
+
--link: #b5d2bc;
|
|
1193
|
+
--success-500: #6cc3ac; --success-100: #14271f; --success-text: #8ad3bf;
|
|
1194
|
+
--warning-500: #d9a94f; --warning-100: #2a2213; --warning-text: #e6c17e;
|
|
1195
|
+
--danger-500: #d9806d; --danger-100: #2c1814; --danger-text: #e9a597;
|
|
1196
|
+
--info-500: #8fb0c9; --info-100: #18222b; --info-text: #a9c4d8;
|
|
1197
|
+
--ring: 0 0 0 3px rgba(159,194,168,.3);
|
|
1198
|
+
--ring-color: rgba(159,194,168,.3);
|
|
1199
|
+
--shadow-xs: none;
|
|
1200
|
+
--shadow-sm: none;
|
|
1201
|
+
--shadow-md: none;
|
|
1202
|
+
--shadow-lg: 0 8px 24px rgba(8,10,9,.3);
|
|
1203
|
+
--shadow-xl: 0 16px 32px rgba(8,10,9,.35);
|
|
1204
|
+
color-scheme: dark;
|
|
1205
|
+
|
|
1206
|
+
--overlay: rgba(0, 0, 0, .6);
|
|
1207
|
+
--tooltip-bg: #e3e8e2;
|
|
1208
|
+
--tooltip-text: #2c3630;
|
|
1209
|
+
--focus-color: var(--accent);
|
|
1210
|
+
--text-on-danger: #151a17;
|
|
1211
|
+
--border-hover: #a6b0a8;
|
|
1212
|
+
--ring-danger: 0 0 0 3px rgba(217,128,109,.25);
|
|
1213
|
+
|
|
1214
|
+
--selection-bg: var(--action-bg);
|
|
1215
|
+
--selection-text: var(--text-on-action);
|
|
1216
|
+
|
|
1217
|
+
/* Series categóricas: orden estable; acompañar con etiqueta o forma. */
|
|
1218
|
+
--chart-1: #60a46f;
|
|
1219
|
+
--chart-2: #a85a3f;
|
|
1220
|
+
--chart-3: #2da79b;
|
|
1221
|
+
--chart-4: #9b5986;
|
|
1222
|
+
--chart-5: #818230;
|
|
1223
|
+
--chart-6: #3678ac;
|
|
1224
|
+
--chart-7: #946b10;
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
---
|
|
1229
|
+
|
|
1230
|
+
## 16. Distribución y consumo
|
|
1231
|
+
|
|
1232
|
+
El sistema se distribuye en varios formatos para poder usarlo desde cualquier stack:
|
|
1233
|
+
|
|
1234
|
+
| Entregable | Ubicación | Uso |
|
|
1235
|
+
|---|---|---|
|
|
1236
|
+
| **Este documento** | `DESIGN-salvia-serena.md` | Fuente de verdad legible |
|
|
1237
|
+
| **Style guide interactivo** | `salvia-serena.html` | Referencia visual siempre disponible (abrir en navegador) |
|
|
1238
|
+
| **Tokens CSS** | `dist/salvia-serena/tokens.css` | Drop-in para cualquier proyecto (vanilla, React, Angular, Vue) |
|
|
1239
|
+
| **Framework CSS** | `dist/salvia-serena/salvia-serena.css` | Tokens + clases de componentes (`.btn`, `.card`, `.sidebar`…) y la clase `.salvia-reset` |
|
|
1240
|
+
| **Tailwind v4** | `dist/salvia-serena/tailwind.css` | Bloque `@theme` — genera utilidades `bg-primary-500`, `text-neutral-800`, etc. |
|
|
1241
|
+
| **DTCG JSON** | `dist/salvia-serena/tokens.json` | Formato estándar W3C DTCG 2025.10 — interoperable con Figma/Tokens Studio/Style Dictionary |
|
|
1242
|
+
| **Paquete npm** | `@higinio/design-css/salvia/…` | Subrutas `salvia/tokens.css`, `salvia/framework.css`, `salvia/tailwind.css` y `salvia/tokens.json` |
|
|
1243
|
+
| **MCP** | `system="salvia"` | Tokens, specs de componentes y validación desde el servidor MCP (`get_token(system="salvia", name="--accent")`) |
|
|
1244
|
+
| **Skill de Claude** | `~/.claude/skills/salvia-serena-design/` | Invocable desde cualquier sesión de Claude Code |
|
|
1245
|
+
|
|
1246
|
+
**Cómo consumirlo en un proyecto nuevo:**
|
|
1247
|
+
|
|
1248
|
+
1. Copiar `dist/salvia-serena/tokens.css` (o importar `@higinio/design-css/salvia/tokens.css`) antes que cualquier estilo propio, **o** pegar el bloque `@theme` en el CSS de Tailwind v4 (`@higinio/design-css/salvia/tailwind.css`). Para tener también las clases de componentes, `@higinio/design-css/salvia/framework.css`.
|
|
1249
|
+
2. Aplicar la clase `.salvia-reset` al `body` (o usar los estilos base equivalentes).
|
|
1250
|
+
3. Los componentes consumen solo aliases semánticos (§3.4) — nunca hex.
|
|
1251
|
+
4. Para light/dark: alternar `data-theme` en `<html>`; los tokens hacen el resto.
|
|
1252
|
+
5. En el `<head>`: `<meta name="theme-color" content="#f3f2ec">` (el `--bg` claro, lino); con el tema oscuro activo, actualizarlo a `#151a17` para que la barra del navegador acompañe al fondo.
|
|
1253
|
+
|
|
1254
|
+
### Mantenimiento de las distribuciones
|
|
1255
|
+
|
|
1256
|
+
Los contratos se definen en esta especificación y sus valores ejecutables en `dist/salvia-serena/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.
|
|
1257
|
+
|
|
1258
|
+
### Series categóricas
|
|
1259
|
+
|
|
1260
|
+
`--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.
|