La misma página de estudio seis veces, una por personalidad de partida: oscura y clara, en oro, malva, verde, bermellón y terracota, con títulos serif, en cursiva y condensados.
La misma página de estudio en las seis personalidades de partida.

Sistema de diseño

Eclipse UI

Una base común, la personalidad de cada estudio.

Eclipse es una plataforma de reservas para estudios de yoga, pilates y entrenamiento. Cada estudio tiene su propia instancia (hoy son unas nueve), todas salen del mismo código. Eclipse UI es el design system que hice para que cada una se vea como su estudio sin bifurcar ese código y sin bajar la calidad.

Diseño del sistema, diseño de interfaz y desarrollo  ·  Una sola persona


Rol
Diseño del sistema, diseño de interfaz y desarrollo.
Stack
Rails 8, ViewComponent, Stimulus y Turbo, Tailwind 4. Sin React y sin paso de build de JavaScript.
Resultado
58 componentes (47 de la base, públicos en el sitio, y 11 del pack de reservas que vive solo en Eclipse), 6 personalidades de partida y un catálogo con 50 fichas en vivo (42 de la base y 8 del pack). Hoy le da forma al sitio público de la demo de Eclipse.
Enlaces
la demo de Eclipse ↗ · el sitio de Eclipse UI ↗

El problema

Un estudio de barre y un box de HIIT no deberían verse iguales, pero en Eclipse lo único que cambiaba de una instancia a otra era el color de marca. Las vistas escribían sus colores a mano: en las 17 vistas públicas había 442 referencias sueltas a variables ([var(--color-…)]), 207 clases de la paleta de Tailwind (gray-500, white…) y 48 íconos pegados como <svg>. Cambiar la voz o la forma de un estudio significaba editar vista por vista, en cada instancia.

Además, la marca de base, un dorado (#a3946b), se usaba como texto y como fondo de botones: medía entre 2.7 y 3.0:1, y WCAG AA pide 4.5:1. En cinco páginas, axe encontró 58 elementos con contraste insuficiente.

Lo que necesitaba era un sistema que separara dos cosas: lo que todo estudio merece por igual y lo que hace a cada uno distinto.


La idea: base y personalidad

La base es lo que no cambia de un estudio a otro, y si un componente rompe una de estas garantías, no está terminado: accesible de verdad (contraste AA medido, foco visible, teclado completo, movimiento reducido respetado); solo tokens y escalas, nunca un color o un tamaño suelto; jerarquía por capas y bordes de 1 px, con la sombra reservada para lo que flota; movimiento físico; integrado con Rails (los controles mandan su valor con name:, funcionan con Turbo y con el form builder), y un código que es de quien lo usa, sin cajas negras.

La personalidad es lo que decide cada estudio, en cinco rasgos:

RasgoQué cambia
Voztipografías, peso, cursiva, mayúsculas y ancho de los títulos; la voz de los botones
Formaredondez, botones y campos en pastilla o rectos, qué tan marcados son los bordes
Colormarca, tinta, papel y cuánto tiñe la marca los grises
Airequé tan compacta o aireada es la interfaz, cuánta sombra lleva lo que flota
Ritmoqué tan rápido se mueve todo y cuánto brilla lo importante

Los rasgos se mueven con 27 diales que caben en un bloque de theme.css. El motor deriva lo demás: la paleta, los hover, el texto que va sobre la marca, las capas, las sombras, los radios y las duraciones. Partí de seis personalidades pensadas para tipos de estudio distintos, y cada una es un punto de partida, no un tema cerrado:

PersonalidadParaCarácter
Carbóninterfaces sobriasmonocroma, Inter y JetBrains Mono, bordes de 1 px
Eclipsela base de la apporo cálido, Fraunces en los títulos, calmada
Albaspa, barre, bienestarmalva, Cormorant en cursiva, todo en pastilla, mucho aire, lenta
Brisapilates clínico, fisioterapiaverde salvia, Manrope, radios chicos, compacta y rápida
Umbrabox, HIIT, cyclingbermellón sobre negro, Archivo condensada en mayúsculas, todo recto
Terrayoga, comunidadterracota, Fraunces suave y Nunito, la marca tiñe los grises

Llevar una a un estudio es copiar su bloque a theme.css y mover los diales que haga falta. Todo lo demás se realinea solo.

En vivo

La misma tarjeta de clase en tres de las seis personalidades de partida. El código de la tarjeta no cambia, solo los diales.

Personalidad

60 min

4/12

Vinyasa Flow

con Ana GarcíaTodos los niveles

8 lugares disponibles

Eclipse. Oro cálido sobre papel, Fraunces en los títulos, calmada. Para marcas cálidas y editoriales.

Alba. Malva, Cormorant en cursiva, todo en pastilla, mucho aire, lenta. Para bienestar, belleza y editorial.

Umbra. Bermellón sobre negro, Archivo condensada en mayúsculas, todo recto, rápida. Para deporte, energía y marcas de alto contraste, en oscuro, su modo natural.


Los guardianes: que la base no dependa de la memoria

Un sistema que promete contraste AA en cualquier combinación de diales no puede revisarse a ojo. Así que la base tiene guardianes que corren con las pruebas:

  • Contraste. Una lista de 22 pares de texto y superficie (config/eclipse_ui/contrast.yml) se mide en un navegador real en las seis personalidades y en el theme.css del estudio, en claro y en oscuro: 308 mediciones en 0.6 s. Las mide un navegador porque el motor deriva los colores con color-mix(in oklab) y oklch(from …), y reimplementar eso en Ruby sería una segunda copia del motor que tarde o temprano diría otra cosa que la pantalla. Si un estudio elige una marca con la que algún texto deja de leerse, la prueba dice cuál.
  • Tokens. Ocho reglas recorren cada componente y su CSS y fallan, con archivo y línea, si aparece un hex, un color de la paleta de Tailwind, una sombra que no sigue el dial, un tamaño de texto en px, un rounded sin escala o una variable escrita a mano donde hay una utilidad con nombre.
  • Catálogo. Cada componente tiene ficha, demo, props, patrón ARIA y teclado. Una pieza solo puede decir "estable" si una vista real de la app la usa.

El contraste dejó de ser una decisión estética: la marca como texto usa text-primary-text, un tono derivado que pasa AA en todas las personalidades (mínimo 5.7:1), y el texto sobre un botón de marca se elige solo (6.0:1 sobre el dorado).

Los tokens, en vivo

Todo lo de abajo lo pinta el mismo motor, con la personalidad Eclipse.

Color

Entran marca, tinta, papel y tinte. Salen superficies, texto, bordes y estados, mezclados en oklab.

Diales

  • Marca--eui-brand · #a3946b
  • Tinta--eui-ink · #1a1a18
  • Papel--eui-paper · #f5f2ec
  • Tinte--eui-tint · 25% de marca en los grises

Superficies y bordes

  • Fondo#f5f2ec
  • Tarjeta#ffffff
  • Elevado#edeae3
  • Relleno#e8e5df
  • Borde sutil#e2dfd9
  • Borde#d6d3cc
  • Borde hover#c4c1b9

Texto y marca

  • Texto#1a1a18
  • Secundario#66635a
  • Metadatos#87847b
  • Marca#a3946b
  • Marca hover#b9aa80
  • Texto de marca#6a5c35
  • Sobre la marca#181612

Estados

  • Éxito#4a7357
  • Aviso#b88a3a
  • Error#b04236

Tipografía

Una fuente de texto para toda la interfaz, una mono para datos y etiquetas, y la voz display de cada personalidad para los títulos. Si un tamaño no está en la escala, no existe.

  • DisplayFraunces · 48pxRespira, muévete, vuelve a ti.
  • DisplayFraunces · 36pxVinyasa Flow
  • Título de páginaInter · 24px · 600Hoy en el estudio
  • Título de secciónInter · 18px · 600Paquetes
  • CuerpoInter · 14pxReserva tu lugar en Vinyasa Flow con Ana García.
  • MetadatosInter · 12px8 lugares disponibles
  • Datos y etiquetasMono · 11px07:00–08:00 · Sala 2 · #R-1042

Espaciado

Un dial de densidad, de 0.85 a 1.2, multiplica rellenos, separaciones y alturas de control. Una marca aireada respira; una compacta aprieta.

  • Relleno sm12px
  • Relleno md16px
  • Relleno lg24px
  • Control sm32px
  • Control md40px
  • Control lg48px

Radios

Un solo dial de redondez multiplica todos los radios, así que toda la app se redondea o se endereza con la marca. Una personalidad puede volver pastilla o rectos los botones y los campos sin tocar las tarjetas.

  • sm6px
  • md8px · botones
  • lg12px · campos
  • xl16px · tarjetas
  • 2xl20px · paneles
  • fullpastillas

Física sin React

Los componentes con vida suelen venir de librerías de React. Eclipse es Rails con Stimulus, así que escribí la física desde cero: spring.js son 127 líneas sin dependencias, con un integrador de paso fijo (1/240 s) que se comporta igual en una pantalla de 60 Hz que en una de 120 Hz.

Gráfica de tres resortes que llegan al 100 % de su destino: responsive en 192 ms con un rebote de 0.9 %, snappy en 300 ms y gentle en 550 ms, ambos sin rebote.
Cómo llega cada resorte a su destino.
  • Cinco ajustes con nombre en lugar de duraciones: snappy llega al 1 % de su destino en 300 ms (botones, indicadores), gentle en 550 ms (pestañas, diálogos) y responsive en 192 ms, con un rebote de 0.9 % que se reserva para los regresos y las pilas de avisos.
  • Conserva la velocidad. Si el destino cambia a medio camino (un arrastre, un botón magnético que sigue al cursor), el resorte no reinicia: sigue desde donde iba. Esa es la diferencia entre algo que se siente físico y algo que se siente animado.
  • Obedece al estudio y a la persona. El dial de ritmo (--eui-motion) escala el tiempo de todos los resortes y de las transiciones CSS, y con prefers-reduced-motion todo salta al final.

Hoy la usan 34 controladores de Stimulus: deslizar para reservar, dígitos que ruedan, pestañas con pastilla, modales que se asientan, avisos que se arrastran.


En la app real: el sitio público de la demo

La prueba de fuego fue vestir el sitio público completo de la demo con los componentes: navegación, pie, avisos, confirmaciones, portada, horario, clase, precios, contacto, acceso, registro, cuenta y Nosotros. Las capturas usan la misma base de datos en las dos versiones; solo cambia el código.

La portada del estudio de la demo antes de Eclipse UI, un héroe oscuro con tarjetas sueltas, y después, un héroe limpio con un medidor de cupo.
Portada, antes y después.
El horario semanal antes y después: el nuevo muestra el estado de cada clase con el ícono de personas y los lugares disponibles.
Horario, antes y después: el estado de cada clase con el ícono de personas.
Los paquetes del estudio de la demo antes y después: las tarjetas nuevas suman el costo por clase y la vigencia.
Precios, antes y después: costo por clase y vigencia en cada paquete.
La portada del estudio de la demo y una clase en el teléfono, antes y después.
En el teléfono: portada y clase.
La página de una clase en el teléfono, antes y después: la nueva junta el cupo y la acción en una tarjeta.
La clase en el teléfono: el cupo y la acción en una sola tarjeta.

Lo que cambió en el código de las vistas públicas:

MétricaAntesDespués
Líneas de ERB1,6951,470
Colores escritos a mano ([var(--color-…)])4420
Clases de la paleta de Tailwind (gray-*, white…)2070
Sombras de Tailwind que no siguen el dial240
<svg> pegados480
Componentes de Eclipse UI usados419

Lo que cambió en accesibilidad (axe 4.10, WCAG 2.1 AA, en portada, horario, clase, precios y acceso, en escritorio y modo claro):

Contraste insuficienteAntesDespués
En el contenido (el dorado como texto o fondo, grises sobre papel)480
En el pie oscuro (texto tenue sobre negro)100
Total580

El pie fue lo último en caer: sus etiquetas y créditos usaban text-faint, a 3.66:1 sobre negro, y el propio sistema dice que text-faint no es para texto que haya que leer. Ahora usan text-muted-foreground. En el teléfono y en modo oscuro, axe todavía marca etiquetas pequeñas del horario y de la clase con ese mismo tono (de 3.4 a 4.3:1); es lo siguiente que hay que alinear.

Cuatro de sus componentes, en vivo

El catálogo público tiene 42, cada uno con su demo en vivo, props, patrón ARIA y teclado. Estos cuatro, con sus estados, en la personalidad Eclipse.

Botón con estados

Cuenta lo que pasa con la acción: gira mientras reserva, dibuja una palomita al terminar y, si algo falla, se sacude y lo dice.

  • Reposo
  • Cargando
  • Éxito
  • Error

Campo de texto

El campo resuelve la etiqueta, la ayuda, el error y el ARIA. Las unidades y los íconos van dentro.

Normal
Foco
Con unidades

Paquete de 10 clases

Error

Reformer: mínimo 45 min

Pastilla de estado

Unifica cómo se ven los estados de pedidos, pagos, citas y suscripciones. El color nunca va solo: el estado siempre se lee como texto.

  • Confirmada
  • Pendiente
  • Cancelada
  • Inasistencia
  • Activo
  • Expirado
  • Agotado
  • Pagado
  • Reembolsado
  • En curso

Estado vacío

Lo que se ve cuando una tabla, una lista o una sección no tiene nada todavía: dice qué pasa, qué hacer y ofrece la acción.

Aún no hay clases esta semana

Programa la primera desde una plantilla y aparecerá en el horario del estudio.

Reservaciones canceladas0

Sin cancelaciones esta semana

Cuando alguien cancele, la verás aquí con su crédito devuelto.


De sistema interno a producto

Eclipse UI nació dentro de Eclipse. Para que sirviera fuera de la app, tenía que dejar de ser parte de ella.

  • Su propio repositorio. Separé Eclipse UI de la app en su propio repositorio, como gema de Rails (eclipse_ui). Eclipse la usa como una copia de solo lectura que se actualiza con un comando (bin/update_eclipse_ui), y unas pruebas impiden editarla dentro de la app. El pack de reservas (clase, horario, paquetes, reservación…) se quedó en Eclipse: es su ventaja frente a otros softwares para estudios.
  • De documentación a página de producto. El sitio público pasó de ser documentación a ser página de producto, bajo la idea "Que tu app no se vea hecha por IA". Arriba, la misma página cambia sola entre las seis personalidades. El movimiento se puede pausar (WCAG 2.2.2) y no arranca solo si la persona pide movimiento reducido. Debajo siguen el catálogo, Sistema y Personalidades: una exportación estática con las demos funcionando sin servidor, que no publica el código de los componentes, solo cómo se usan.
  • Fuera de Rails. Desde el sitio se descarga gratis, sin cuenta: el motor en un solo CSS (eclipse-ui.css) y, por personalidad, su theme.css, su shadcn.css y su DESIGN.md.
  • El shadcn.css se mide. Lleva los valores de shadcn/ui medidos en un navegador real sobre el motor y convertidos a OKLCH. La razón es la misma que con el contraste: medir lo que pinta la pantalla en lugar de reimplementar el motor.
  • El DESIGN.md es para la IA. Son instrucciones para que un agente de IA (Claude Code, Cursor) aplique la personalidad en cada pantalla: qué tokens usar, qué reglas seguir y qué haría que la app se viera hecha por IA.
La portada del sitio de Eclipse UI: el titular
La portada de Eclipse UI: "Que tu app no se vea hecha por IA".
El catálogo de Eclipse UI: una rejilla de tarjetas de componentes, cada una con su demo en vivo.
El catálogo: cada componente con su demo en vivo.
La ficha de un componente en el sitio de Eclipse UI, Deslizar para reservar, con un escenario que cambia de personalidad y de modo.
La ficha de un componente: el escenario cambia de personalidad y de modo.

Lo que aprendí

  • Nombrar los rasgos fue más útil que agregar diales. Con "voz, forma, color, aire y ritmo", un dueño de estudio puede decir qué quiere cambiar sin hablar de CSS, y cada pedido cae en un dial concreto.
  • Un guardián vale más que una regla en un documento. Las reglas de tokens y de contraste corren con las pruebas: un componente que las rompe no pasa, lo haya escrito una persona o una IA.
  • La guía también es para la IA. La misma guía (docs/GUI.md) y unas instrucciones para el agente que escribe la UI hacen que el código nuevo salga ya con tokens, con física y accesible.
  • Un design system interno se vuelve producto cuando sirve fuera de su app. Y hoy quien más necesita una personalidad de diseño es quien construye con IA.

Lo que sigue: llevar el panel del estudio (lo que el dueño usa todos los días) a Eclipse UI y probar en Safari y Firefox reales.

Cómo se midió: las capturas y axe 4.10.3 corrieron en Chromium con Playwright, a 1440 × 900 y 390 × 844, contra el commit 99cdd64 (antes de Eclipse UI) y contra main, con la misma base de datos. El conteo del código corre las reglas de Ui::Guardrails sobre las mismas vistas públicas en los dos commits (17 archivos antes; 19 después, porque dos parciales salieron de ellas). Los tiempos de los resortes simulan spring.js con su propio integrador y un recorrido de 100 px.