Sistema de diseño · Higinio Sosa
Salvia Serena
Sistema de marca light-first, natural y relajante, para los productos de bienestar, hábitos y salud de Higinio Sosa: web, móvil, dashboards y landing pages. La calma viene del color, del aire y de un ritmo pausado, no de la decoración.
Calma antes que urgencia
Nada parpadea, nada apremia. El sistema acompaña; nunca presiona ni cuenta hacia atrás.
Claridad primero
El contenido siempre por encima de la decoración.
Un acento a la vez
El salvia guía la acción; nunca compite consigo mismo y nunca comunica éxito.
La naturalidad la ponen el color y la tipografía
Lino, salvia y una serif cálida. Sin hojas dibujadas ni texturas de papel.
Movimiento pausado y con propósito
Algo más lento que lo habitual, porque relaja; la animación explica, nunca decora sola.
Regla de oro
Los componentes consumen solo tokens semánticos (--surface, --accent, --link…), nunca hex directos. El cambio de tema es un atributo, no un refactor.
Fundamentos
Voz y contenido
Serena, cercana y natural: acompaña sin presionar. Cálida sin ser empalagosa, natural sin ser rústica.
| Regla | Sí | No |
|---|---|---|
| Idioma: español LatAm neutro, de "tú" | Guarda tu registro | Guarde usted su registro |
| Sentence case en todo | Registrar comida | Registrar Comida / REGISTRAR |
| Botones imperativos y tranquilos, de 1–3 palabras | Más tarde | Guardando tu registro ahora mismo |
| Sin urgencia: los plazos se dicen con calma | Puedes registrarlo hasta las 23:00 | ¡Última oportunidad! |
| Sin culpa: se cuenta lo hecho, no lo que falta | 3 de 8 vasos | Te faltan 5 |
| Errores que dicen qué hacer, sin dramatizar | No se pudo guardar. Revisa tu conexión e inténtalo de nuevo. | Error |
| Estados vacíos que invitan sin presionar | Aún no registras comidas hoy. Añade la primera cuando quieras. | No hay datos |
| Logros sobrios, sin exclamaciones ni emoji | Meta del día cumplida | ¡Lo lograste! 🎉 |
| Voz en segunda persona | Tus hábitos | Nuestros planes para ti |
Fundamentos
Colores
Un solo acento — salvia #4f6b58 — sobre neutrales de lino derivados de la tinta verde #2c3630. Haz clic en cualquier muestra para copiar su variable.
Primario — Salvia
Neutrales — Lino
Semánticos — ramp de 3 tonos
Cada estado tiene el sólido (-500), el fondo suave (-100) y el tono de texto AA sobre ese fondo (-text). Atención y error son tonos tierra; info es un azul pizarra propio, para no diluir el acento.
Aliases semánticos
Los componentes consumen solo esta capa — así el cambio de tema es un cambio de atributo.
| Alias | Valor (light) | Rol |
|---|
Regla AA del acento
primary-500 sobre --surface da 5.62:1, pero el texto de links y botones 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.
Regla de éxito
El acento salvia nunca comunica éxito. Un resultado positivo usa --success-* (verde azulado) + icono ✓ + texto. Un botón salvia y una comida «completada» no pueden verse iguales.
Proporción 60-30-10
Un solo botón primario visible por pantalla; el resto secundarios o ghost. Fondos siempre planos de lino: sin gradientes, texturas de papel, granulado ni ilustraciones de hojas. El acento pierde poder si supera el 10 %. Estado nunca solo con color: color + icono + texto, y el éxito nunca con el salvia del acento.
Fundamentos
Tipografía
Tres familias con roles estrictos. Escala 1.25 (tercera mayor) desde 16 px.
Lora
Display 600–700 · solo h1–h2, cifras hero y títulos de marketing. Nunca UI ni párrafos. La itálica 600 queda para un énfasis editorial puntual.
Nunito Sans
Body/UI 400–700 · párrafos, botones, formularios, h3–h6. Sus terminales suaves mantienen la UI amable y legible.
Source Code Pro
Mono 400–500 · datos numéricos, código, IDs, cantidades en tablas.
Escala
Tracking: -0.02em en display ≥31px · 0 en body · +0.04em en labels uppercase. Longitud de línea máx. 65ch. Números en tablas con tabular-nums. Mínimo absoluto 12 px.
Fundamentos
Espaciado
Escala base 4 px — cero valores mágicos. El número del token = px ÷ 4.
Aliases de uso
| Alias | Valor | Uso |
|---|---|---|
| --card-padding | 24px | Padding de tarjetas (16px en móvil) |
| --section-gap | 64px | Separación entre secciones |
| --form-gap | 20px | Gap entre campos de formulario |
| --button-padding-md | 12px 20px | Botón md (sm: 8px 14px) |
| --control-height-* | 36 / 44 / 52 | Alturas de controles sm / md / lg |
| --gutter | 24px | Gutter del grid de 12 columnas (16px móvil) |
Fundamentos
Bordes, radios y focus
Bordes de 1 px, radios amplios y un focus siempre visible. Las esquinas suaves bajan la tensión visual sin volver la UI infantil.
Radios
Borde por defecto 1px solid var(--border). Focus ring global de teclado: 2px var(--focus-color) (el acento, en los dos modos) con offset 2px vía :focus-visible; el click de ratón no lo dispara. Único uso de transparencia: overlay de modal y ring de focus. No hay blur ni glassmorphism en Salvia Serena: las superficies son opacas y planas.
Fundamentos
Elevación
Sombras suaves teñidas con la tinta verde rgba(44,54,48,…), nunca negro puro. Máximo 3 niveles de elevación visibles por pantalla. Sin resplandor de acento: la elevación es sombra y borde, nunca brillo.
Niveles
reposo · sin sombra
shadow-xs/sm · inputs, tarjetas
shadow-md · hover, dropdowns
shadow-lg/xl · modales, diálogos
Sombras
Fundamentos
Iconografía
Set sustituto: Lucide — trazo 1.5–2 px y esquinas redondeadas, que casan con las formas suaves y el ritmo tranquilo de la marca. Si existe un set propio de marca, reemplazar.
- Los iconos se insertan inline como SVG para heredar currentColor.
- Tamaños: 14 px inline con texto pequeño · 16–18 px inputs y chevrons · 20 px botones y toolbars · 40 px empty states (en primary-300 decorativo).
- El ✓ es del éxito: check / circle-check acompañan siempre a un estado de éxito, en el tono --success-*.
- Iconos botánicos (leaf, sprout…) solo cuando representan un dato real, como una categoría «verduras»; nunca como adorno de marca.
- Sin emoji; sin Unicode-como-icono (excepto el / tipográfico de breadcrumbs).
Fundamentos
Marca / wordmark
No hay logo ni imágenes de marca. Donde un mockup necesite logotipo se renderiza «Higinio» (o el nombre del producto) en --font-display (Lora) — nunca se dibuja un logo aproximado.
- Lora 600 (--weight-semibold), tracking --tracking-display y 31 px de referencia: el 600 deja ver el trazo caligráfico sin endurecerlo; el 700 queda para titulares grandes.
- Variante con descriptor: 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.
- Variante badge: caja --surface con borde --border, radius md y shadow-sm.
- El wordmark es texto, no un icono: nunca en UI ni párrafos.
- Sentence case siempre: «Higinio», nunca «HIGINIO» ni «higinio».
- Color --text-primary (tinta verde); primary-300 jamás como texto, tampoco en el wordmark.
- Sin hojas, ramas ni isotipo botánico junto al nombre. No se genera ilustración para no inventar assets: se usan placeholders y se piden los materiales reales.
Biblioteca
Componentes
Los 35 componentes del sistema, renderizados en vivo con las clases del framework salvia-serena.css. Cada bloque incluye su código de uso.
Botones
Alturas 36 / 44 / 52 px · radius md · peso 600 · un solo primario visible por pantalla. Ghost y outline usan --link (AA), no primary-500. Confirmar es una acción y va en salvia: ningún botón se pinta de --success-500.
Ver código
<button class="btn btn--primary">Guardar cambios</button>
<button class="btn btn--secondary">Cancelar</button>
<button class="btn btn--outline">Exportar</button>
<button class="btn btn--ghost">Ver detalles</button>
<button class="btn btn--danger">Eliminar cuenta</button>
<!-- Tamaños: btn--sm · (md por defecto) · btn--lg -->
<button class="btn btn--primary btn--lg">Grande</button>
IconButton
Botón cuadrado solo-icono para toolbars y chrome. Default ghost. Siempre con aria-label.
Ver código
<button class="icon-btn icon-btn--ghost" aria-label="Buscar">
<svg>…icono 18px…</svg>
</button>
<!-- Variantes: --ghost (default) · --secondary · --primary
Tamaños: --sm 32px · (md 44px) · --lg 48px -->
Spinner
Trazo 2.5, rotación 0.8 s linear. Si la carga dura <300 ms, no mostrar spinner. Blanco dentro de botones primarios.
Ver código
<svg class="spinner" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2.5"/>
<path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor"
stroke-width="2.5" stroke-linecap="round"/>
</svg>
Input
Altura 44 px · label siempre visible (nunca placeholder como label) · focus con borde --accent + ring · obligatorio con .field--required (asterisco --danger-text + required/aria-required).
Ver código
<div class="field">
<label class="field__label" for="correo">Correo electrónico</label>
<input class="input" id="correo" type="email" placeholder="tu@correo.com">
<span class="field__help">Tu correo no se comparte con nadie.</span>
</div>
<!-- Obligatorio: .field--required en el .field + required/aria-required en el input -->
<div class="field field--required">
<label class="field__label" for="nombre">Nombre completo</label>
<input class="input" id="nombre" required aria-required="true">
</div>
<!-- Estados: input--error (+ .field__error) · input--success · disabled
Icono leading: envolver en .input-icon -->
Textarea
Min-height 96 px · resize: vertical · contador opcional alineado a la derecha.
Ver código
<div class="field">
<label class="field__label" for="desc">Descripción</label>
<textarea class="textarea" id="desc" maxlength="200"></textarea>
<span class="textarea-counter">0 / 200</span>
</div>
Select
Trigger igual que input + chevron 16 px que rota al abrir. Menú con shadow-lg y opción seleccionada en --accent-soft.
Ver código
<div class="select-wrap">
<select class="select">
<option>Ciudad de México (GMT-6)</option>
<option>Bogotá (GMT-5)</option>
</select>
</div>
SegmentedControl
2–4 opciones mutuamente excluyentes. Indicador blanco con shadow-xs. Para más de 4 opciones, usar Select.
Ver código
<div class="segmented" role="radiogroup" aria-label="Vista">
<input type="radio" name="vista" id="lista" checked><label for="lista">Lista</label>
<input type="radio" name="vista" id="tablero"><label for="tablero">Tablero</label>
</div>
<!-- Compacto: añadir segmented--sm -->
Slider
Track 6 px con porción rellena en --accent · thumb blanco 18 px con borde de acento que escala 1.15 al presionar.
Ver código
<div class="slider-row">
<span class="slider-label">Límite de gasto</span>
<span class="slider-value">$ 350</span>
</div>
<input class="slider" type="range" min="0" max="1000" value="350">
<!-- JS: actualizar --slider-fill = ((v-min)/(max-min))*100 + '%' -->
Checkbox y Radio
20 px · borde 2 px · check que se dibuja con ease-spring · label clicable con gap 10 px · estado indeterminate (guion) para «seleccionar todo» parcial · marcar es seleccionar, no un estado de éxito.
Ver código
<label class="checkbox">
<input type="checkbox" checked>
<span class="checkbox__box">
<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>
</span>
Recibir novedades
</label>
<label class="radio">
<input type="radio" name="plan" checked>
<span class="radio__dot"></span>
Plan mensual
</label>
<!-- Indeterminate: propiedad del DOM, no atributo HTML -->
<!-- checkbox.indeterminate = true (guion horizontal) -->
Switch
Track 44×24 px · thumb 20 px que desliza 20 px en 220 ms ease-spring.
Ver código
<label class="switch">
<input type="checkbox" checked>
<span class="switch__track"></span>
Notificaciones
</label>
Badge y Tag
Fondo suave semántico (*-100) + texto --*-text (AA garantizado). Estado nunca solo con color: color + icono + texto. El badge de éxito lleva siempre su ✓.
Ver código
<span class="badge badge--success">
<svg>…icono…</svg>Activo
</span>
<!-- Tonos: (neutral) · --success · --warning · --danger · --info -->
<span class="tag">diseño
<button class="tag__close" aria-label="Quitar etiqueta">
<svg>…x…</svg>
</button>
</span>
Avatar
Fallback automático a iniciales sobre chip primary-100. Dot de presencia: 28 % del diámetro con anillo de superficie.
Ver código
<span class="avatar avatar--lg">
<span class="avatar__initials">HS</span>
<i class="avatar__dot avatar__dot--online"></i>
</span>
<!-- Tamaños: --sm 28 · (md 36) · --lg 48 · --xl 64
Presencia: --online · --busy · --away · (offline) -->
AvatarGroup
Stack de avatares solapados (-8px) con anillo 2 px de --surface que los separa · avatar-group__more para el +N de overflow.
Ver código
<div class="avatar-group">
<span class="avatar"><span class="avatar__initials">HS</span></span>
<span class="avatar"><span class="avatar__initials">MG</span></span>
<span class="avatar"><span class="avatar__initials">AL</span></span>
<span class="avatar"><span class="avatar__initials">RD</span></span>
<span class="avatar-group__more">+3</span>
</div>
<!-- Solape -8px · cada avatar con anillo 2px de --surface -->
Divider
Regla hairline 1 px --border · variante con label centrado (patrón "o continúa con") · vertical para flex.
Ver código
<hr class="divider">
<div class="divider--label" role="separator">o continúa con</div>
<span class="divider--vertical" role="separator"></span>
Accordion
Altura animada con grid-template-rows 0fr→1fr (sin reflow) · chevron rota 180° · aria-expanded.
Ver código
<div class="accordion">
<div class="accordion__item is-open">
<button class="accordion__header" aria-expanded="true">
Pregunta <svg>…chevron…</svg>
</button>
<div class="accordion__panel"><div>
<div class="accordion__content">Respuesta</div>
</div></div>
</div>
</div>
<!-- JS: alternar .is-open en .accordion__item + aria-expanded -->
Tooltip
Delay 400 ms · fade + translateY(4→0) · máx. 1 línea; si necesita más, usar popover. Pasa el cursor por los botones.
Ver código
<button class="btn btn--secondary" data-tooltip="Duplica el proyecto">
Duplicar
</button>
<!-- Solo CSS: atributo data-tooltip. Fondo se invierte en dark. -->
Alert
Banner inline y persistente (vs Toast, transitorio). Icono *-500 + texto --*-text sobre fondo *-100. role="status". En éxito el ✓ no es opcional.
Nueva versión disponible
La versión 2.4 incluye exportación a PDF. Actualiza cuando quieras.
Pago confirmado
El recibo se envió a tu correo.
Tu plan se renueva el 12 de julio
Puedes revisar tu forma de pago hasta esa fecha.
No se pudo guardar el formulario
Revisa los campos marcados e inténtalo de nuevo.
Ver código
<div class="alert alert--warning" role="status">
<svg>…icono…</svg>
<div>
<p class="alert__title">Tu plan se renueva el 12 de julio</p>
<p class="alert__body">Puedes revisar tu forma de pago hasta esa fecha.</p>
</div>
<button class="alert__close" aria-label="Cerrar aviso">…x…</button>
</div>
<!-- Tonos: --info · --success · --warning · --danger -->
Toast
Top-right (desktop) · 360 px · borde izquierdo 3 px semántico · auto-dismiss 5 s pausable con hover · máx. 3 apilados.
Ver código
<div class="toast-stack" role="status" aria-live="polite">
<div class="toast toast--success">
<svg>…icono…</svg>
<div>
<p class="toast__title">Cambios guardados</p>
<p class="toast__body">El proyecto se actualizó.</p>
</div>
<button class="icon-btn icon-btn--ghost" aria-label="Cerrar">…x…</button>
</div>
</div>
Progress y ProgressRing
Solo progreso determinado — para carga indeterminada usar Skeleton. Width / dashoffset animan 280 ms. Mientras se avanza, el relleno es --accent; solo al cumplir la meta pasa a success-500, con ✓ y el texto que lo dice.
Ver código
<div class="progress-row">
<span class="progress-label">Almacenamiento</span>
<span class="progress-value">68%</span>
</div>
<div class="progress">
<div class="progress__bar" style="width:68%"
role="progressbar" aria-valuenow="68"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
<!-- Tonos de barra: (accent) · --success · --warning · --danger -->
Modal
role="dialog" · aria-modal · aria-labelledby + aria-describedby · al abrir el foco entra al panel, queda atrapado (Tab/Shift+Tab ciclan) y vuelve al disparador al cerrar · Esc y click en overlay cierran, salvo flujos destructivos (el de confirmación no cierra por overlay).
Ver código
<div class="modal" id="miModal" role="dialog" aria-modal="true"
aria-labelledby="miModalTitulo" aria-describedby="miModalDesc">
<!-- overlay sin data-modal-close en flujos destructivos -->
<div class="modal__overlay" data-modal-close></div>
<div class="modal__panel" tabindex="-1">
<div class="modal__header">
<h4 class="modal__title" id="miModalTitulo">¿Eliminar proyecto?</h4>
<button class="icon-btn icon-btn--ghost" data-modal-close aria-label="Cerrar">…x…</button>
</div>
<p id="miModalDesc">Esta acción no se puede deshacer.</p>
<div class="modal__footer">
<button class="btn btn--secondary" data-modal-close>Cancelar</button>
<button class="btn btn--danger">Eliminar</button>
</div>
</div>
</div>
<!-- Panel ancho: modal__panel--lg (640px) -->
<!-- JS: al abrir guarda el disparador y enfoca el panel; Tab/Shift+Tab
atrapados dentro; al cerrar (Esc, ×, Cancelar) el foco vuelve al disparador -->
Tabla
Header uppercase +0.04em · filas 52 px · numéricos a la derecha con tabular-nums. Variantes: .table--zebra · .table--sticky (header fijo) · columna ordenable con aria-sort · selección con .row--selected y «seleccionar todo» indeterminate.
| Cliente | Estado | ||
|---|---|---|---|
| Constructora Delta | Activo | $ 48,200.00 | |
| Grupo Andrade | Pendiente | $ 9,850.00 | |
| Studio Nébula | Activo | $ 132,400.00 | |
| Farmacias Luna | Vencido | $ 1,120.00 |
Ver código
<table class="table table--zebra table--sticky">
<thead>
<tr>
<th class="col-select" scope="col">
<label class="checkbox"><input type="checkbox" aria-label="Seleccionar todo">
<span class="checkbox__box"><svg>…check…</svg></span></label>
</th>
<th scope="col">Cliente</th>
<th class="num" scope="col" aria-sort="none">
<button class="th-sort th-sort--num" data-sort="monto">
Monto<svg class="sort-icon" aria-hidden="true">…</svg></button>
</th>
</tr>
</thead>
<tbody>
<tr class="row--selected">
<td class="col-select"><label class="checkbox"><input type="checkbox" checked
aria-label="Seleccionar Grupo Andrade">…</label></td>
<td>Grupo Andrade</td><td class="num" data-monto="9850">$ 9,850.00</td>
</tr>
</tbody>
</table>
<!-- JS: click en .th-sort alterna aria-sort asc/desc y reordena tbody;
el checkbox del header pasa a indeterminate con selección parcial -->
Stat
KPI único, componer dentro de Card. Valor en mono text-3xl · el delta indica tendencia, no éxito: neutro por defecto (--text-secondary), y --up / --down solo cuando el producto decide qué sentido es bueno · count-up al entrar en viewport.
Ver código
<div class="card">
<div class="stat">
<span class="stat__label"><svg>…</svg>Ingresos del mes</span>
<div class="stat__row">
<span class="stat__value">$ 128,400</span>
<span class="stat__delta stat__delta--up"><svg>…</svg>+12.4%</span>
</div>
</div>
</div>
<!-- Delta: neutro por defecto (text-secondary) · --up (success-text) y
--down (danger-text) solo cuando el producto decide qué sentido es bueno -->
Chip de métrica
Pastilla de una línea con rótulo y valor en --link · fila con gap que salta de línea en móvil · el chip interactivo es un <button> de 44 px con nombre accesible completo · al llegar a la meta lo dicen un ✓ en --success-500 y el nombre accesible, no el salvia del valor. Pulsa «Agua» hasta llegar a 8.
Ver código
<div class="metric-chips">
<span class="metric-chip">
<span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span>
</span>
<button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
<svg aria-hidden="true">…plus…</svg>
<span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
</button>
</div>
<!-- El aria-label se actualiza con el valor; empieza por el texto visible -->
<!-- Meta cumplida: .metric-chip--met pinta el ✓ en --success-500 y el nombre accesible lo dice -->
<button class="metric-chip metric-chip--met" type="button"
aria-label="Agua: 8 de 8 vasos, meta cumplida. Empezar de nuevo">
<svg aria-hidden="true">…check…</svg>
<span class="metric-chip__label">Agua</span> <span class="metric-chip__value">8 de 8</span>
</button>
Pagination
1-indexado, bajo una tabla · trunca con elipsis a partir de 7 páginas · página activa como chip sólido con aria-current="page".
Ver código
<ul class="pagination" aria-label="Paginación">
<li><button class="pagination__btn" aria-label="Anterior">…‹…</button></li>
<li><button class="pagination__btn">1</button></li>
<li><span class="pagination__ellipsis">…</span></li>
<li><button class="pagination__btn is-active" aria-current="page">5</button></li>
<li><button class="pagination__btn">6</button></li>
<li><button class="pagination__btn" aria-label="Siguiente">…›…</button></li>
</ul>
Skeleton y EmptyState
Bloque plano que pulsa en opacidad (1 → 0.5, 1.36 s por tramo, loop alternado), sin degradado; radius igual al contenido que reemplaza. Un estado vacío siempre invita a actuar, sin presionar.
Aún no registras comidas hoy
Añade la primera cuando quieras.
Ver código
<div class="skeleton" style="height:10px;width:85%"></div>
<div class="skeleton skeleton--circle" style="width:40px;height:40px"></div>
<div class="empty-state">
<svg>…icono 40px…</svg>
<p class="empty-state__title">Aún no registras comidas hoy</p>
<p class="empty-state__desc">Añade la primera cuando quieras.</p>
<button class="btn btn--primary">Registrar comida</button>
</div>
Card, Link y Code
Tarjeta con padding 24 px y shadow-sm; la interactiva sube 2 px al hover. Link con subrayado que crece. Code inline en mono.
Tarjeta estática
Contenido en reposo con borde --border y shadow-sm.
Tarjeta interactiva
Pasa el cursor: sube 2 px con shadow-md. Consulta la guía de motion.
Ver código
<div class="card">…</div>
<div class="card card--interactive">…</div>
<a class="link" href="#">Enlace con subrayado animado</a>
<span class="code">--accent</span>
Combobox y búsqueda
role="combobox" · aria-expanded + aria-controls + aria-activedescendant · lista role="listbox", opciones role="option" · ↑/↓ mueven, Enter elige, Esc cierra, Home/End a extremos. La búsqueda se limpia con la ×.
Ver código
<div class="combobox">
<input class="combobox__input" type="text" role="combobox"
aria-expanded="false" aria-controls="lista" aria-autocomplete="list"
aria-activedescendant="opt-0" autocomplete="off">
<ul class="combobox__list" id="lista" role="listbox" aria-label="Ciudades">
<li class="combobox__option is-active" role="option" id="opt-0"
aria-selected="false">Guadalajara</li>
<li class="combobox__option" role="option" id="opt-1"
aria-selected="true">Monterrey
<svg class="combobox__check">…check…</svg></li>
</ul>
</div>
<div class="search">
<svg class="search__icon" aria-hidden="true">…lupa…</svg>
<input class="search__input" type="search" placeholder="Buscar…">
<button class="search__clear" aria-label="Limpiar">…x…</button>
</div>
Drawer / Sheet
role="dialog" · aria-modal · foco atrapado y devuelto al disparador (patrón del Modal) · overscroll-behavior: contain · entra deslizando desde el borde y sale más corto · Esc y click en overlay cierran. Variantes --right (default), --left, --bottom.
Ver código
<div class="drawer drawer--right" id="miDrawer" role="dialog"
aria-modal="true" aria-labelledby="miDrawerTitulo">
<div class="drawer__overlay" data-drawer-close></div>
<div class="drawer__panel" tabindex="-1">
<div class="drawer__header">
<h4 class="drawer__title" id="miDrawerTitulo">Filtrar proyectos</h4>
<button class="icon-btn icon-btn--ghost" data-drawer-close aria-label="Cerrar">…x…</button>
</div>
<!-- campos… -->
<div class="drawer__footer">
<button class="btn btn--secondary" data-drawer-close>Cancelar</button>
<button class="btn btn--primary" data-drawer-close>Aplicar</button>
</div>
</div>
</div>
Popover
Se abre por click (no hover, a diferencia del tooltip) y lleva contenido interactivo · trigger aria-haspopup="dialog" + aria-expanded · panel role="dialog" · aparece desde el trigger (fade + scale 0.96→1) · Esc y click fuera cierran.
Plan Estudio
Incluye 12 tableros, 3 integraciones y respaldo diario. Puedes cambiar de plan cuando quieras.
Ver comparación de planesVer código
<div class="popover">
<button class="btn btn--secondary popover__trigger"
aria-haspopup="dialog" aria-expanded="false"
aria-controls="pop">Detalles del plan</button>
<div class="popover__panel" id="pop" role="dialog"
aria-label="Detalles del plan" tabindex="-1">
<p>Contenido con <a class="link" href="#">un enlace</a>.</p>
</div>
</div>
ButtonGroup
Botones adyacentes con bordes compartidos (radius solo en los extremos, bordes internos colapsados) · role="group" + aria-label · usa los .btn existentes. Para un único-seleccionado, usa SegmentedControl.
Ver código
<div class="btn-group" role="group" aria-label="Navegación de semana">
<button class="btn btn--secondary">Anterior</button>
<button class="btn btn--secondary">Hoy</button>
<button class="btn btn--secondary">Siguiente</button>
</div>
DatePicker
Input + calendario en popover · rejilla role="grid", encabezados role="columnheader" con aria-label completo, días role="gridcell" con aria-label de la fecha entera y aria-selected · las flechas mueven entre días · el input abre el calendario (click), Esc cierra y el foco entra al calendario · hoy marcado y un día seleccionado.
Ver código
<div class="datepicker">
<input class="datepicker__input" type="text" readonly
aria-haspopup="dialog" aria-controls="cal">
<svg class="datepicker__icon" aria-hidden="true">…calendario…</svg>
<div class="datepicker__calendar" id="cal" role="dialog"
aria-label="Elegir fecha" tabindex="-1">
<div class="calendar">
<div class="calendar__header">
<button aria-label="Mes anterior">…</button>
<span class="calendar__title">julio 2026</span>
<button aria-label="Mes siguiente">…</button>
</div>
<div class="calendar__table" role="grid" aria-label="julio 2026">
<div class="calendar__weekdays" role="row">
<span class="calendar__weekday" role="columnheader"
aria-label="lunes">lu</span> …
</div>
<div class="calendar__grid" role="rowgroup">
<div class="calendar__week" role="row">
<button class="calendar__day calendar__day--today" role="gridcell"
aria-label="viernes, 10 de julio de 2026"
aria-selected="false">10</button>
<button class="calendar__day calendar__day--selected" role="gridcell"
aria-label="viernes, 17 de julio de 2026"
aria-selected="true">17</button> …
</div>
</div>
</div>
</div>
</div>
</div>
FileUpload
Dropzone + lista · el input file real va oculto pero con label asociado y aria-describedby de formatos y tamaño · la zona describe la acción · estado .is-dragover al arrastrar · el error no se comunica solo por color (icono + texto) · el item que sube reusa la barra .progress.
Ver código
<div class="file-upload">
<input class="file-upload__input" type="file" multiple
aria-describedby="hint">
<label class="file-upload__label" for="…">
<svg aria-hidden="true">…subir…</svg>
<span class="file-upload__title">Arrastra archivos o elige</span>
<span class="file-upload__button">Elegir archivos</span>
<span class="file-upload__hint" id="hint">PDF, PNG o JPG · hasta 10 MB</span>
</label>
</div>
<ul class="file-upload__list">
<li class="file-upload__item">
<svg class="file-upload__icon">…</svg>
<div class="file-upload__meta">
<span class="file-upload__name">informe.pdf</span>
<span class="file-upload__size">2.4 MB · Subiendo 45%</span>
<div class="progress"><div class="progress__bar" style="width:45%"></div></div>
</div>
<button class="file-upload__remove" aria-label="Cancelar informe.pdf">×</button>
</li>
<li class="file-upload__item file-upload__item--error">
<svg class="file-upload__icon">…alerta…</svg>
<div class="file-upload__meta">
<span class="file-upload__name">video.mov</span>
<span class="file-upload__size">…alerta… Formato no admitido</span>
</div>
<button class="file-upload__remove" aria-label="Quitar video.mov">×</button>
</li>
</ul>
Stepper / Wizard
<ol> · aria-current="step" en el paso actual · el completo muestra un check (no solo color), el próximo muestra su número · estados --complete/--current/--upcoming · .step__connector entre pasos · variante --vertical · un paso completo marca avance y va en salvia; el resultado del flujo se confirma aparte, con el tono de éxito y su ✓.
- Cuenta
- Perfil
- 3 Facturación
- 4 Confirmar
- Borrador guardado
- 2 Revisión del equipo
- 3 Publicación
Ver código
<ol class="stepper" aria-label="Progreso de configuración">
<li class="step step--complete">
<span class="step__connector" aria-hidden="true"></span>
<span class="step__marker">…check…</span>
<span class="step__label">Cuenta</span>
</li>
<li class="step step--current" aria-current="step">
<span class="step__connector" aria-hidden="true"></span>
<span class="step__marker">3</span>
<span class="step__label">Facturación</span>
</li>
<li class="step step--upcoming">
<span class="step__marker">4</span>
<span class="step__label">Confirmar</span>
</li>
</ol>
<!-- Vertical: añade la clase --vertical al <ol> -->
<ol class="stepper stepper--vertical">…</ol>
Kbd
Tecla en <kbd> con borde, sombra sutil de tinta y tipografía mono · tamaño pequeño, mínimo 20px · combos con "+" entre teclas.
Inline en el texto: abre la paleta con ⌘ K y guarda con Ctrl S.
Ver código
<kbd class="kbd">Esc</kbd>
<!-- Combo: "+" literal entre teclas -->
<span class="kbd-combo">
<kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd>
</span>
Sistema
Motion
Pausado y con propósito: las entradas son algo más largas que en un sistema utilitario, porque un ritmo pausado relaja; el feedback táctil sigue siendo inmediato. Solo transform y opacity, la salida ~20 % más corta que la entrada y distancias de 4–8 px.
| Token | Valor | Uso |
|---|---|---|
| --duration-instant | 100ms | Press, toggle |
| --duration-fast | 150ms | Hover, focus, color |
| --duration-base | 220ms | Dropdowns, tooltips, switches |
| --duration-slow | 280ms | Modales, toasts, drawers |
| --duration-page | 340ms | Transiciones de página o vista (techo) |
Easings (pasa el cursor)
- Stagger en listas: 30 ms por ítem, máximo 6 ítems.
- Si la carga dura <300 ms, no mostrar spinner (evita parpadeo).
- Ningún indicador late en loop (pulse, ping, glow): un latido sugiere urgencia, y aquí nada urge. El único pulso es el del skeleton, mientras carga.
- prefers-reduced-motion colapsa toda animación a 0.01 ms.
Sistema
Accesibilidad
WCAG AA mínimo: 4.5:1 texto normal, 3:1 texto grande y componentes UI. Touch targets de 44×44 px. Estado nunca comunicado solo con color.
| Combinación | Contraste | Veredicto |
|---|---|---|
| neutral-800 sobre --surface | 11.99:1 | AAA |
| Blanco sobre primary-500 | 5.87:1 | AA texto normal |
| --link (primary-600) sobre --surface | 7.00:1 | AA texto normal |
| primary-400 sobre --surface | 3.35:1 | Solo iconos y UI |
| primary-300 sobre --surface | 2.23:1 | Nunca como texto |
| Tonos --*-text sobre *-100 | 5.33–6.28:1 | AA garantizado |
| Éxito frente a acento (--success-500 / --accent) | ΔE76 15.5 | Distinguibles (mín. 15) |
- Focus visible siempre: ring global de teclado vía :focus-visible con especificidad cero (:where()).
- IconButton siempre con aria-label · Progress con role="progressbar" · Alert con role="status" · Pagination con aria-current="page".
- Labels de formulario siempre visibles — el placeholder nunca sustituye al label.
Sistema
Tokens y descarga
El sistema completo vive en dist/salvia-serena/: salvia-serena.css (framework completo), tokens.css (solo variables), tailwind.css (@theme para Tailwind v4) y tokens.json (W3C DTCG 2025.10, interoperable con Figma / Tokens Studio / Style Dictionary).
Variables CSS
:root {
/* Primario (Salvia) */
--primary-50: #e2eadf; --primary-100: #d4e0d1; --primary-200: #b8cbb7;
--primary-300: #97b09e; --primary-400: #73907c; --primary-500: #4f6b58;
--primary-600: #435c4b; --primary-700: #384d3f; --primary-800: #304236;
--primary-900: #2c3630;
/* Neutrales (Lino) */
--neutral-0: #fbfaf6; --neutral-50: #f3f2ec; --neutral-100: #eae9e1;
--neutral-200: #dfe0d6; --neutral-300: #b9beb4; --neutral-400: #858d84;
--neutral-500: #5c675f; --neutral-600: #47524b; --neutral-700: #38423b;
--neutral-800: #2c3630; --neutral-900: #1f2622;
/* Semánticos (ramp de 3) */
--success-500: #2f7d6a; --success-100: #dcefe8; --success-text: #22604f;
--warning-500: #b8862f; --warning-100: #f6ecd8; --warning-text: #7e5a17;
--danger-500: #b5523f; --danger-100: #f6e4df; --danger-text: #8a3a2b;
--info-500: #5b7891; --info-100: #e3eaf0; --info-text: #3f5a70;
/* Aliases (los componentes consumen SOLO esto) */
--bg: var(--neutral-50); --surface: var(--neutral-0); --surface-alt: var(--neutral-100);
--border: var(--neutral-200); --border-strong: var(--neutral-400);
--text-primary: var(--neutral-800); --text-secondary: var(--neutral-600);
--text-muted: var(--neutral-500); --text-disabled: var(--neutral-400);
--text-on-primary: #ffffff; --text-on-primary-solid: var(--primary-700);
--accent: var(--primary-500); --accent-hover: var(--primary-600);
--accent-active: var(--primary-700); --accent-soft: var(--primary-50);
--accent-soft-strong: var(--primary-100); --link: var(--primary-600);
/* Tipografía */
--font-display: "Lora", Georgia, serif;
--font-body: "Nunito Sans", system-ui, -apple-system, sans-serif;
--font-mono: "Source Code Pro", ui-monospace, monospace;
--text-xs: .75rem; --text-sm: .875rem; --text-base: 1rem; --text-lg: 1.125rem;
--text-xl: 1.25rem; --text-2xl: 1.5625rem; --text-3xl: 1.9375rem;
--text-4xl: 2.4375rem; --text-5xl: 3.0625rem;
--tracking-display: -0.02em; --tracking-label: 0.04em; --measure: 65ch;
/* Espaciado (base 4px) */
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
--space-12: 48px; --space-16: 64px; --space-20: 80px; --space-24: 96px;
--control-height-sm: 36px; --control-height-md: 44px; --control-height-lg: 52px;
/* Radios y sombras (teñidas de tinta verde) */
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px;
--radius-xl: 24px; --radius-full: 9999px;
--shadow-xs: 0 1px 2px rgba(44,54,48,.06);
--shadow-sm: 0 1px 3px rgba(44,54,48,.08), 0 1px 2px rgba(44,54,48,.05);
--shadow-md: 0 4px 8px rgba(44,54,48,.09), 0 2px 4px rgba(44,54,48,.05);
--shadow-lg: 0 12px 24px rgba(44,54,48,.12), 0 4px 8px rgba(44,54,48,.06);
--shadow-xl: 0 24px 48px rgba(44,54,48,.16);
--ring: 0 0 0 3px rgba(79,107,88,.25);
/* Motion (pausado: nada urge) */
--duration-instant: 100ms; --duration-fast: 150ms; --duration-base: 220ms;
--duration-slow: 280ms; --duration-page: 340ms;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
}
Tailwind CSS v4 — bloque @theme
@import "tailwindcss";
@import "./tokens.css";
@theme {
/* Colores → genera bg-primary-500, text-neutral-800, border-primary-200… */
--color-primary-50: #e2eadf; --color-primary-100: #d4e0d1;
--color-primary-200: #b8cbb7; --color-primary-300: #97b09e;
--color-primary-400: #73907c; --color-primary-500: #4f6b58;
--color-primary-600: #435c4b; --color-primary-700: #384d3f;
--color-primary-800: #304236; --color-primary-900: #2c3630;
--color-neutral-0: #fbfaf6; --color-neutral-50: #f3f2ec;
--color-neutral-100: #eae9e1; --color-neutral-200: #dfe0d6;
--color-neutral-300: #b9beb4; --color-neutral-400: #858d84;
--color-neutral-500: #5c675f; --color-neutral-600: #47524b;
--color-neutral-700: #38423b; --color-neutral-800: #2c3630;
--color-neutral-900: #1f2622;
--color-success-500: #2f7d6a; --color-success-100: #dcefe8; --color-success-700: #22604f;
--color-warning-500: #b8862f; --color-warning-100: #f6ecd8; --color-warning-700: #7e5a17;
--color-danger-500: #b5523f; --color-danger-100: #f6e4df; --color-danger-700: #8a3a2b;
--color-info-500: #5b7891; --color-info-100: #e3eaf0; --color-info-700: #3f5a70;
/* Tipografía → font-display, font-body, font-mono */
--font-display: "Lora", Georgia, serif;
--font-body: "Nunito Sans", system-ui, sans-serif;
--font-mono: "Source Code Pro", ui-monospace, monospace;
/* Radios → rounded-sm/md/lg/xl */
--radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 24px;
/* Sombras → shadow-xs…shadow-xl (teñidas de tinta verde) */
--shadow-xs: 0 1px 2px rgba(44,54,48,.06);
--shadow-sm: 0 1px 3px rgba(44,54,48,.08), 0 1px 2px rgba(44,54,48,.05);
--shadow-md: 0 4px 8px rgba(44,54,48,.09), 0 2px 4px rgba(44,54,48,.05);
--shadow-lg: 0 12px 24px rgba(44,54,48,.12), 0 4px 8px rgba(44,54,48,.06);
--shadow-xl: 0 24px 48px rgba(44,54,48,.16);
/* Easings → ease-out-brand, ease-in-brand, ease-in-out-brand, ease-spring */
--ease-out-brand: cubic-bezier(0.2, 0, 0, 1);
--ease-in-brand: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out-brand: cubic-bezier(0.65, 0, 0.35, 1);
--ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1);
}
Cómo consumirlo en un proyecto nuevo
1) Instala @higinio/design-css (e importa @higinio/design-css/salvia/framework.css) o copia dist/salvia-serena/salvia-serena.css (o solo tokens.css) e impórtalo antes de tus estilos, o pega el bloque @theme en tu CSS de Tailwind v4. 2) Aplica .salvia-reset al body. 3) Consume solo aliases semánticos. 4) Alterna data-theme en <html> para light/dark. 5) Declara <meta name="theme-color" content="#f3f2ec"> (el lino de --bg) y cámbialo a #151a17 con el tema oscuro.
Sistema
Instalación
El sistema se distribuye como paquete npm (@higinio/design-css) y también como archivos sueltos en dist/salvia-serena/. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C). En todas: light por defecto; <html data-theme="dark"> activa el modo oscuro, y los componentes consumen solo aliases semánticos, nunca hex.
Instalar desde npm (recomendado)
Un solo paquete con todos los sistemas, cada uno en su subruta; Salvia Serena vive en /salvia/. Instálalo y usa las vías A/B/C apuntando a la subruta, en vez de a un archivo copiado.
npm i @higinio/design-css
/* framework completo (vía A) */
@import "@higinio/design-css/salvia/framework.css";
/* o solo tokens (vía B) */
@import "@higinio/design-css/salvia/tokens.css";
/* o Tailwind v4 (vía C) */
@import "@higinio/design-css/salvia/tailwind.css";
A · CSS drop-in (framework completo)
Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes. Aplica .salvia-reset al <body>.
<link rel="stylesheet" href="/estilos/salvia-serena.css">
<body class="salvia-reset">
<button class="btn btn--primary">Guardar</button>
<div class="card">…</div>
</body>
B · Solo tokens
Si diseñas tus propios componentes: solo las variables, sin clases.
@import "./tokens.css";
.mi-panel {
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border);
}
C · Tailwind v4
Tailwind v4 se configura por CSS. El tailwind.css ya incluye @import "tailwindcss", los tokens y el bloque @theme — úsalo como tu hoja de entrada (mantén tokens.css a su lado, que es lo que importa).
# 1) instala Tailwind v4
npm i tailwindcss @tailwindcss/postcss
# 2) copia tailwind.css + tokens.css a tu carpeta de CSS
# y usa tailwind.css como entrada. Ya tienes utilidades:
<div class="bg-primary-500 text-neutral-0">…</div>
D · Pipelines de tokens
Para Figma, Tokens Studio o Style Dictionary: importa tokens.json (formato estándar W3C DTCG 2025.10).
Con Claude no copias nada
Invoca la skill salvia-serena-design o conecta el MCP (sección siguiente) y construye consumiendo tokens, componentes y reglas del sistema, con validación automática.
Sistema
MCP
El paquete @higinio/design-mcp expone los sistemas de diseño de Higinio Sosa, entre ellos Salvia Serena (system="salvia"), a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.
Las 11 tools
| Tool | Qué hace |
|---|---|
| list_design_systems | Lista los sistemas de diseño disponibles. |
| get_token | Resuelve un alias semántico o un token DTCG por path a su valor. |
| search_tokens | Busca tokens por nombre, categoría o valor. |
| list_components | Lista los componentes documentados de un sistema. |
| get_component_spec | Devuelve la especificación completa de un componente: estados, medidas, variantes. |
| get_rules | Devuelve las reglas duras (no negociables) del sistema. |
| validate_code | Audita CSS/HTML contra las reglas del sistema — hex directos, contraste, aliases. |
| validate_motion | Audita el motion contra las reglas del sistema: transition sobre todas las propiedades, ease-in en entradas, propiedades de layout, duraciones por encima de --duration-page. |
| check_slop | Detecta clichés de diseño generado por IA: gradientes, glassmorphism, sombras de negro puro, fuentes genéricas y copy de marketing. |
| checklist_preflight | Checklist estático de pre-producción sobre HTML/CSS: metadatos, headings, imágenes y accesibilidad de controles. |
| generate_snippet | Genera un snippet de componente listo para usar, con los aliases correctos. |
Conectar (remoto)
Añade el servidor a tu .mcp.json:
{
"mcpServers": {
"design": {
"type": "http",
"url": "https://design-mcp.higiniolab.tech/mcp",
"headers": { "Authorization": "Bearer <tu-token>" }
}
}
}
Ejemplo
Consultar un token de este sistema:
get_token(system="salvia", name="--accent")
→ "#4f6b58"
Auditar código antes de integrarlo:
validate_code(system="salvia", code="…")
→ { ok: false, issues: ["hex directo #4f6b58 en línea 12 — usa var(--accent)"] }
MCP y skills, misma fuente de verdad
Usa las skills dentro de Claude; el MCP para IDEs y clientes MCP externos. Ambos leen DESIGN-salvia-serena.md.