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:

ArchivoQué llevaLí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.

ClaveObligatoriaPara 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.

Los cinco tonos
Pasada En vivo Confirmada Pago pendiente Cancelada
TonoCuándoTokens
:neutralInformativo, sin juicio: pasada, programada, borrador. Es el valor por defecto.bg-muted · text-muted-foreground · punto bg-subtle
:accentLo nuevo, lo en vivo, lo de la marca.bg-primary · text-primary-text · bg-wash
:successSalió bien: confirmada, pagado, con check-in.bg-success · text-success
:warningPide atención pronto: pendiente, vence en 3 días, lista de espera.bg-warning · text-warning
:dangerError, 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 de acciones
EscaleraValores, de más a menos pesoRegla
Acciones:primary · :secondary · :outline · :ghost · :linkUna sola :primary por vista o por tarjeta; la que la marca quiere que hagas.
Marcas e indicadores:solid · :soft · :outlineBadge 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.

Controles en sm, md y lg
sm
md
lg
TamañoAlturaUtilidad
: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ámetroValoresQué decideHoy lo aceptan
shape::segment · :pillLa 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 · :relaxedEl aire entre filas de una colección.DataTable
padding::none · :sm · :md · :lgEl aire dentro de un contenedor.Card, SpotlightCard
on::background · :card · :raisedLa superficie de abajo, para que un anillo o una insignia la corten.AvatarStack, NotificationBell
side: / align::top · :bottom · :left · :right / :start · :center · :endDó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ámetroSignificaHoy 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:

SlotParaLo 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 nameformattributelabelhinterrorrequireddisabled
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: (con unchecked_value:) para un booleano.
  • Atributos extra: en un control de una sola pieza nativa (Input, Select, Textarea, Checkbox) **attrs llega al elemento nativo (autocomplete:, inputmode:); en los compuestos llega a la raíz y el control nativo recibe input: {}.
  • Enviar solo: lo que manda su formulario al completarse usa requestSubmit, así Turbo lo maneja y se pide con auto_submit: (fechas y código; submit_on_change: y submit: 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:

VerboEventosNota
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:

AtributoHace
data-eui · data-eui-modePone 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, -holdPide confirmación con Confirmación en lugar del confirm() del navegador.
data-autofocusLo que recibe el foco al abrir una capa.
data-sheet-no-dragUna 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óndeAntesAhora
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 (includes en el controlador) o lo que llega por parámetro (price:, items:); nunca un where ni un count adentro. 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 atajo href: + cta_label: existe para el caso simple.
  • Los controles mandan su valor. Con name: o form: + 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 escucha turbo:submit-start y turbo:submit-end; lo que flota se cierra en turbo: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.

Estable 13 de 42

Ya se usa en vistas reales de una app en producción. Su API solo cambia con alias que aceptan el nombre anterior.

Beta 29 de 42

Lista, con prueba, ficha y demo revisadas, pero todavía sin uso en la app. Su API aún puede ajustarse.

Experimental 0 de 42

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: estable en 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

  1. 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.
  2. 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.tone o Ui::Vocabulary.pick: así los alias funcionan y un typo falla en desarrollo.
  3. Crea los archivos de la anatomía. Copia la forma de slide_to_confirm_*: cabecera con qué es, cuándo usarlo y un ejemplo.
  4. 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.
  5. Elige el patrón ARIA en la tabla de accesibilidad y completa el teclado.
  6. Conéctalo a Rails: name: / form: + attribute:, Turbo y eventos componente:verbo.
  7. Escribe la ficha con status: beta, pattern: y keyboard:, y una demo con contenido real que luzca en la tarjeta de 330px y en el escenario.
  8. Prueba y corre los guardianes (ver Calidad): tu prueba, la del catálogo, la de las páginas y RuboCop.
  9. Míralo en carbón claro y oscuro, eclipse y umbra, y a 375px de ancho, con la consola limpia.
  10. Documenta en docs/GUI.md si agregaste un componente, un token o un parámetro del vocabulario.