Ya se usa en vistas reales de una app en producción. Su API solo cambia con alias que aceptan el nombre anterior.
Convenciones
Componentes
El vocabulario común de todas las APIs: tono, tamaño, variante, slots y formularios.
Eclipse UI tiene 42 fichas en el catálogo y todas hablan el mismo idioma: el peligro se dice igual en un botón que en un aviso, el tamaño chico mide lo mismo en un campo que en un botón, y un control de formulario se conecta al modelo de la misma forma. Si sabes usar una pieza, ya sabes usar las demás. Esta página es ese idioma.
Anatomía de un componente
Un componente son unos cuantos archivos legibles, siempre los mismos y siempre en el mismo lugar. Así se ve el de referencia, Deslizar para reservar:
| Archivo | Qué lleva | Líneas |
|---|---|---|
components/ui/slide_to_confirm_component.rb |
La clase: parámetros con su valor por defecto y las clases de Tailwind. La cabecera dice qué es, cuándo usarlo y trae un ejemplo. | 70 |
components/ui/slide_to_confirm_component.html.erb |
El markup, cuando pasa de unas 15 líneas (si no, va en #call). | 17 |
javascript/controllers/slide_to_confirm_controller.js |
El comportamiento en Stimulus: resortes, teclado, eventos, Turbo. | 109 |
assets/tailwind/eclipse_ui/slide_to_confirm.css |
Solo lo que Tailwind no alcanza: keyframes y pseudo-elementos, dentro de @layer components. | 20 |
views/ui/catalog/slide_to_confirm.yml |
La ficha: título, categoría, estado, props, accesibilidad y los archivos que muestra. | 36 |
views/ui/catalog/_slide_to_confirm.html.erb |
La demo en vivo con contenido real de estudio; su código es el «Uso» de la ficha. | 4 |
test/components/ui/slide_to_confirm_component_test.rb |
La prueba: lo que pinta, su ARIA y cada variante o estado. | 18 |
Ningún componente nuevo nace sin los siete. El JavaScript y el CSS solo existen si hacen falta; la ficha, la demo y la prueba, siempre.
La ficha
La ficha (app/views/ui/catalog/<nombre>.yml) es lo que el catálogo lee para
armar la página del componente. catalog_test.rb revisa las claves marcadas como
obligatorias en cada ficha, en cada corrida de pruebas.
| Clave | Obligatoria | Para qué |
|---|---|---|
title |
sí | El nombre en español, como lo diría la marca (Deslizar para reservar). |
category |
sí | Estudio, Admin, Comercio, Botones, Gestos, Datos vivos, Avisos, Capas, Navegación o Formularios. |
status |
sí | estable, beta o experimental. Ver «Estados» abajo. |
tagline |
sí | Una línea: qué hace, no cómo. |
description |
no | El párrafo de arriba de la ficha: cuándo usarlo y qué lo hace distinto. |
component |
no | La clase (Ui::…Component). Con ella la prueba comprueba el estado contra el uso real. |
files |
sí, si muestra código | Las pestañas de código; solo de app/components/ui, controllers, eclipse_ui y tailwind. |
props |
no | Nombre, tipo, valor por defecto y descripción de cada parámetro. |
pattern / keyboard |
sí | El patrón ARIA y las teclas en una línea cada uno; arman la tabla de Accesibilidad. |
accessibility / features |
no | Notas en viñetas; entrecomilla las que lleven dos puntos o #. |
El vocabulario
Cada eje tiene un nombre y una lista corta de valores. Un componente nuevo usa estos nombres o no entra. La columna «hoy lo aceptan» se lee de los parámetros de cada clase al pintar esta página: si alguien agrega o quita un parámetro, la tabla cambia sola.
Tono: tone:
El significado del color, nunca su peso. Son cinco y no hay más: si algo no
encaja en uno, es :neutral.
| Tono | Cuándo | Tokens |
|---|---|---|
:neutral | Informativo, sin juicio: pasada, programada, borrador. Es el valor por defecto. | bg-muted · text-muted-foreground · punto bg-subtle |
:accent | Lo nuevo, lo en vivo, lo de la marca. | bg-primary · text-primary-text · bg-wash |
:success | Salió bien: confirmada, pagado, con check-in. | bg-success · text-success |
:warning | Pide atención pronto: pendiente, vence en 3 días, lista de espera. | bg-warning · text-warning |
:danger | Error, cancelado o irreversible: eliminar, cobro rechazado. | bg-destructive · text-destructive |
El token se sigue llamando destructive (viene de shadcn); en las APIs se dice
siempre danger. Hoy aceptan tone:: Alert, Badge, Button, SlideToConfirm, StatusPill, Toast, Toaster::Toast.
Todos lo normalizan con Ui::Vocabulary.tone, así :destructive,
:error o :alert llegan como :danger en cualquiera (también en
JavaScript: eui:toast y data-confirm-tone); ver
nombres anteriores.
Variante: variant:
Cuánto pesa la superficie, nunca el color. Hay dos escaleras: una para acciones
y otra para marcas e indicadores. Tono y variante se combinan: el botón de peligro es
variant: :primary, tone: :danger.
| Escalera | Valores, de más a menos peso | Regla |
|---|---|---|
| Acciones | :primary · :secondary · :outline · :ghost · :link | Una sola :primary por vista o por tarjeta; la que la marca quiere que hagas. |
| Marcas e indicadores | :solid · :soft · :outline | Badge y el indicador del control segmentado. :soft es el lavado del tono. |
Hoy aceptan variant:: ActionButton, Badge, Button, PressButton, SegmentedControl. Las piezas que la
usaban para otra cosa ya tienen su nombre (shape: en AnimatedTabs,
layout: en Accordion, mode: en Dropzone) y aceptan
variant: como nombre anterior.
Tamaño: size:
:xs · :sm · :md · :lg · :xl, con
:md por defecto. En los controles, cada tamaño es una altura con nombre que sigue
el dial de densidad, así un botón y un campo del mismo tamaño quedan alineados.
| Tamaño | Altura | Utilidad |
|---|---|---|
:sm |
2rem × densidad | h-control-sm |
:md |
2.5rem × densidad | h-control-md |
:lg |
3rem × densidad | h-control-lg |
En las capas (Modal, Sheet, Popover) size: es el ancho preset y un ancho libre va en
width:. Avatares y el medidor del eclipse tienen su propia escala con los mismos
nombres. Dos excepciones a propósito: StatusPill va de :xs a :sm y el
control segmentado arranca en :sm, porque viven en barras compactas.
Forma, disposición, aire y lo que hay detrás
| Parámetro | Valores | Qué decide | Hoy lo aceptan |
|---|---|---|---|
shape: | :segment · :pill | La silueta: radio de control o rounded-full. | AnimatedTabs, SegmentedControl |
layout: | según la pieza (:card · :banner) | Cómo se acomodan las partes, no cuánto pesan. | Accordion, Alert |
density: | :compact · :regular · :relaxed | El aire entre filas de una colección. | DataTable |
padding: | :none · :sm · :md · :lg | El aire dentro de un contenedor. | Card, SpotlightCard |
on: | :background · :card · :raised | La superficie de abajo, para que un anillo o una insignia la corten. | AvatarStack, NotificationBell |
side: / align: | :top · :bottom · :left · :right / :start · :center · :end | Dónde se abre una capa anclada respecto a su botón. | DropdownMenu, Popover, Sheet, Tooltip, NotificationBell |
Celebration usaba on: para otra cosa (el evento que la dispara): ahora es
event:, y on: sigue sirviendo.
Estado
| Parámetro | Significa | Hoy lo aceptan |
|---|---|---|
disabled: |
No se puede usar (el atributo nativo, con su estilo). | ActionButton, Button, Checkbox, ColorField, DateRangePicker, Dropzone, Input, NumberStepper, OtpInput, PressButton, RadioCards, Select, SlideToConfirm, SpringSelect, Switch, Textarea, RadioCards::Option, SegmentedControl::Option |
required: |
Obligatorio: el campo lleva asterisco y required. | ColorField, DateRangePicker, Dropzone, Field, Input, NumberStepper, RadioCards, Select, Textarea |
error: |
El mensaje de error; implica aria-invalid y el borde de error. | ColorField, DateRangePicker, Dropzone, Field, Input, NumberStepper, OtpInput, RadioCards, Select, Textarea |
value: |
El valor libre: lo que escribe la persona o el dato que se muestra. | AnimatedNumber, Checkbox, ColorField, DateRangePicker, Input, NumberStepper, OtpInput, StatCard, Switch, Textarea, RadioCards::Option, SegmentedControl::Option |
selected: |
Cuál opción de un conjunto está elegida (su value). | AnimatedTabs, RadioCards, SegmentedControl, Select, SpringSelect |
checked: |
El booleano de una casilla o un interruptor. | Checkbox, Switch |
open: |
Abierto al pintar. | Modal, Sheet, Accordion::Item |
current: |
Este es el actual (aria-current): el plan que ya tienes. | PricingCard |
status: |
El estado de dominio, tal como lo trae el modelo (confirmed, :reserved…). | StatusPill, ActivityFeed::Item |
Los estados que cambian en el navegador no son parámetros: el controlador los escribe en
data-state (open, closing, loading,
success, error, confirmed…) y el CSS los lee de ahí, junto con
data-tone (solo para el tono) y data-side (el lado de una capa).
Slots
Lo que una pieza no puede saber (la acción real, el botón que abre una capa, el pie de un
modal) entra por slot. Leídos de los renders_one y renders_many de cada clase:
| Slot | Para | Lo tienen |
|---|---|---|
with_action |
La acción principal de una tarjeta de dominio: una. | EmptyState, PricingCard |
with_actions |
Un grupo de botones (aviso, encabezado de página). | Alert, PageHeader |
with_trigger |
Lo que abre una capa: modal, hoja, menú, popover. | DropdownMenu, Modal, Popover, Sheet |
with_header |
La cabeza de un contenedor. | Card, DropdownMenu |
with_footer |
El pie: botones de un modal, totales de una tabla. | Card, DataTable, Modal, Sheet |
with_title |
El título cuando un String no basta. | Modal, Sheet |
with_description |
La descripción cuando un String no basta. | Modal, Sheet |
with_icon |
Reemplaza el ícono por defecto. | Alert, EmptyState |
with_leading |
Lo que va antes del texto de un campo. | Input |
with_trailing |
Lo que va después del texto de un campo. | Input |
with_toolbar |
Búsqueda y filtros encima de una tabla. | DataTable |
with_empty |
Lo que se ve cuando no hay filas (un estado vacío). | DataTable |
with_tabs |
Pestañas o filtros debajo del título de la página. | PageHeader |
with_item |
Una colección: se llama como un elemento, en singular, y se repite. | Accordion, ActivityFeed |
with_notification |
Una colección: se llama como un elemento, en singular, y se repite. | NotificationBell |
with_option |
Una colección: se llama como un elemento, en singular, y se repite. | RadioCards, SegmentedControl |
with_tab |
Una colección: se llama como un elemento, en singular, y se repite. | AnimatedTabs |
Si hay un String y un slot para lo mismo (title: y with_title), gana el
slot. Las colecciones se llaman como un elemento, en singular: with_item,
with_tab, with_option, with_column.
Formularios
Un control se conecta igual en todas partes: name: a mano, o form: +
attribute: con el builder, de donde salen el nombre, el id, el valor y el error del
modelo. La ayuda se llama hint:. La tabla se lee de cada clase; un nombre en gris es
el que esa pieza usa en lugar del canónico.
| Control | name | form | attribute | label | hint | error | required | disabled |
|---|---|---|---|---|---|---|---|---|
| Checkbox | sí | sí | sí | sí | sí | no | no | sí |
| ColorField | sí | sí | sí | sí | sí | sí | sí | sí |
| DateRangePicker | sí | sí | sí | sí | sí | sí | sí | sí |
| Dropzone | sí | sí | sí | sí | sí | sí | sí | sí |
| Input | sí | sí | sí | sí | sí | sí | sí | sí |
| NumberStepper | sí | sí | sí | sí | sí | sí | sí | sí |
| OtpInput | sí | sí | sí | sí | sí | sí | no | sí |
| PricingCard | sí | no | no | sí |
description
|
no | no | no |
| RadioCards | sí | sí | sí | sí | sí | sí | sí | sí |
| SegmentedControl | sí | sí | sí | sí | no | no | no | no |
| Select | sí | sí | sí | sí | sí | sí | sí | sí |
| SpringSelect | sí | sí | sí | sí | no | no | no | sí |
| Switch | sí | sí | sí | sí | sí | no | no | sí |
| Textarea | sí | sí | sí | sí | sí | sí | sí | sí |
- Valores:
value:para lo libre,selected:para elegir entre opciones,checked:(conunchecked_value:) para un booleano. - Atributos extra: en un control de una sola pieza nativa (Input, Select, Textarea, Checkbox)
**attrsllega al elemento nativo (autocomplete:,inputmode:); en los compuestos llega a la raíz y el control nativo recibeinput: {}. - Enviar solo: lo que manda su formulario al completarse usa
requestSubmit, así Turbo lo maneja y se pide conauto_submit:(fechas y código;submit_on_change:ysubmit:siguen sirviendo).
Eventos
Un componente avisa lo que pasó con <componente>:<verbo> (Stimulus pone el
prefijo), con el verbo en presente y detail.value cuando hay un valor. Las órdenes de
afuera hacia adentro van con eui:: eui:open y eui:close en un
dialog, eui:toast en window. Leídos de los controladores:
| Verbo | Eventos | Nota |
|---|---|---|
action |
toaster:action |
— |
change |
animated-tabs:change color-field:change date-range-picker:change dropzone:change number-stepper:change otp-input:change segmented-control:change spring-select:change |
— |
close |
confirm-dialog:close dropdown-menu:close modal:close popover:close sheet:close |
— |
complete |
otp-input:complete |
— |
confirm |
slide-to-confirm:confirm |
— |
confirmed |
slide-to-confirm:confirmed |
Nombre anterior de slide-to-confirm:confirm: se emiten los dos mientras se migra. |
dismiss |
alert:dismiss |
— |
dismissed |
alert:dismissed |
Nombre anterior de alert:dismiss: se emiten los dos mientras se migra. |
open |
confirm-dialog:open dropdown-menu:open modal:open notification-bell:open popover:open sheet:open |
— |
read |
notification-bell:read |
— |
select |
dropdown-menu:select |
— |
Clases y atributos
Todo componente acepta **attrs en su raíz: class: se suma (nunca
reemplaza), data: y aria: se mezclan sin pisar el
data-controller propio e id: se respeta. Si el look sirve fuera del
componente, la clase expone .classes(**) con los mismos parámetros
(Ui::ButtonComponent.classes(variant: :secondary) para un button_to).
Todavía sin **attrs: Toast.
Los atributos públicos que se pueden poner en cualquier elemento:
| Atributo | Hace |
|---|---|
data-eui · data-eui-mode | Pone una personalidad (y un modo) a una sección. |
data-eui-open="<id>" | Abre ese modal u hoja desde cualquier parte; un enlace con href sigue sirviendo sin JS. |
data-turbo-confirm + data-confirm-title, -accept, -cancel, -tone, -hold | Pide confirmación con Confirmación en lugar del confirm() del navegador. |
data-autofocus | Lo que recibe el foco al abrir una capa. |
data-sheet-no-drag | Una zona de la hoja que no inicia el arrastre. |
Nombres anteriores
Cuando un nombre cambia, el anterior sigue sirviendo: el componente lo acepta en la entrada y
pinta exactamente lo mismo, así ninguna vista se rompe. Esta tabla sale de
Ui::Vocabulary y su prueba comprueba que cada clase acepte los dos. En código nuevo,
usa siempre el de la derecha.
| Dónde | Antes | Ahora |
|---|---|---|
| Alert | variant: |
tone: |
| Alert | dismiss_label: |
close_label: |
| Toast | variant: |
tone: |
| SegmentedControl | tone: |
variant: |
| AnimatedTabs | variant: |
shape: |
| Accordion | variant: |
layout: |
| Popover | width: |
size: |
| PageHeader | heading_level: |
heading: |
| DataTable | row_href: |
href_for: |
| Dropzone | current: |
attached: |
| Dropzone | variant: |
mode: |
| DateRangePicker | submit_on_change: |
auto_submit: |
| OtpInput | description: |
hint: |
| OtpInput | submit: |
auto_submit: |
| Checkbox | description: |
hint: |
| Switch | description: |
hint: |
| Celebration | on: |
event: |
| todos · tone: | :destructive, :error, :alert |
:danger |
| todos · tone: | :default, :notice |
:neutral |
| Button · variant: | :destructive, :danger |
variant: :primary, tone: :danger |
| Badge · variant: | :default, :secondary, :destructive, :success, :warning, :wash |
tone: + variant: :solid | :soft | :outline |
| SlideToConfirm · tone: | :default |
:neutral |
| SegmentedControl · variant: | :subtle |
:soft |
| DataTable#with_column · align: | :left, :right |
:start, :end |
| evento | slide-to-confirm:confirmed |
slide-to-confirm:confirm (se emiten los dos) |
| evento | alert:dismissed |
alert:dismiss (se emiten los dos) |
Reglas de API
- Un componente no consulta. Lee lo que ya viene cargado (
includesen el controlador) o lo que llega por parámetro (price:,items:); nunca unwhereni uncountadentro. Los de un pack pueden recibir el modelo completo con un constructor.for(modelo), con la misma regla. - Las acciones reales van por slot. El componente no sabe si reservar es un
button_to, un enlace o un deslizador; la vista sí. El atajohref:+cta_label:existe para el caso simple. - Los controles mandan su valor. Con
name:oform:+attribute:, siempre con un input real (visible u oculto). Sin JavaScript el formulario se envía igual. - Turbo de fábrica. Lo que envía usa
requestSubmit; lo que espera al servidor escuchaturbo:submit-startyturbo:submit-end; lo que flota se cierra enturbo:before-cache; los ids que apunta un Turbo Stream son estables. - Nada se traga un error. Un valor que no existe (
tone: :rojo) falla en desarrollo y pruebas con la lista de valores válidos (Ui::Vocabulary.pick); en producción deja un aviso en el log y usa el valor por defecto. - Textos con valor por defecto en español y un parámetro
<parte>_label:para cambiarlos (close_label:,loading_label:).
Una vista de la app
<%# Los datos entran ya cargados; la acción real, por slot. %>
<%= render Ui::PricingCardComponent.new(name: @plan.name, price: @plan.price, period: "mes", featured: true) do |card| %>
<% card.with_action do %>
<%= form_with url: checkout_path(@plan) do %>
<%= render Ui::SlideToConfirmComponent.new(label: "Desliza para pagar") %>
<% end %>
<% end %>
<% end %>
<%# Un control conectado al modelo: nombre, valor y error salen de f. %>
<%= render Ui::InputComponent.new(form: f, attribute: :capacity, label: "Capacidad", trailing: "lugares") %>
Estados de las fichas
Cada ficha dice qué tan lista está la pieza. El estado sale en la tarjeta del índice y arriba
de cada ficha, y la app que usa la gema lo vigila con Ui::Catalog.app_usages: una ficha
estable debe usarse en alguna de sus vistas, y una pieza que ya se usa debe decir estable.
Lista, con prueba, ficha y demo revisadas, pero todavía sin uso en la app. Su API aún puede ajustarse.
Una propuesta en prueba: puede cambiar o desaparecer. Todavía no va en una vista de la app.
- De beta a estable: el día que una vista de una app en producción la use, cambia
status: estableen su ficha (si no, la prueba de esa app te lo pide). Desde ahí, su API solo cambia con alias. - De experimental a beta: cuando tiene prueba propia, su ficha completa y pasó la revisión en las personalidades y a 375px.
- Nada baja de estable sin un plan de migración para las vistas que la usan.
Agregar un componente, paso a paso
- Busca primero en el catálogo y en
app/components/ui/. Extender una pieza (un parámetro, un slot) casi siempre gana a crear otra. - Nombra con el vocabulario. Antes de escribir código, decide los parámetros con las tablas de arriba: tono, variante, tamaño, slots, formulario, eventos. Normaliza cada valor con
Ui::Vocabulary.toneoUi::Vocabulary.pick: así los alias funcionan y un typo falla en desarrollo. - Crea los archivos de la anatomía. Copia la forma de
slide_to_confirm_*: cabecera con qué es, cuándo usarlo y un ejemplo. - Solo tokens y la escala. Colores, tamaños de texto, radios, sombras y duraciones con nombre (ver Color, Tipografía y Movimiento); íconos del set.
- Elige el patrón ARIA en la tabla de accesibilidad y completa el teclado.
- Conéctalo a Rails:
name:/form:+attribute:, Turbo y eventoscomponente:verbo. - Escribe la ficha con
status: beta,pattern:ykeyboard:, y una demo con contenido real que luzca en la tarjeta de 330px y en el escenario. - Prueba y corre los guardianes (ver Calidad): tu prueba, la del catálogo, la de las páginas y RuboCop.
- Míralo en carbón claro y oscuro, eclipse y umbra, y a 375px de ancho, con la consola limpia.
- Documenta en
docs/GUI.mdsi agregaste un componente, un token o un parámetro del vocabulario.