Admin
Estado: BetaTabla del admin
La tabla de usuarios, pedidos y reservaciones del admin con el lenguaje del sistema: capas, líneas de 1px, hover de fila y cifras alineadas. Se arma por columnas (cada una con un bloque que pinta su celda), trae celdas de ayuda para persona, estado, fecha y dinero, y en móvil se desplaza sola en horizontal sin desbordar la página.
- Admin
- Turbo
- Teclado
- Container queries
| Persona | Rol | Estado | Reservas | Registro |
|---|---|---|---|---|
| Ana GarcíaNuevo, ana.garcia@correo.mx | Cliente | Activo | 14 | |
| Iván López, ivan@lunaestudio.mx | Equipo | Activo | 0 | |
| Mariana PeñaNuevo, mariana.pena@correo.mx | Cliente | Activo | 1 | |
| Sofía Ruiz, sofia@lunaestudio.mx | Equipo | Activo | 0 | |
| Diego Hernández, diego.hdz@correo.mx | Cliente | Inactivo | 23 | |
| Lucía TorresNuevo, lucia.torres@correo.mx | Cliente | Activo | 2 | |
| Regina Castro, regina@lunaestudio.mx | Admin | Activo | 0 | |
| Tomás NavarroNuevo, tomas.navarro@correo.mx | Cliente | Activo | 8 | |
Nadie coincidePrueba con otro nombre, correo o rol. |
||||
Mostrando 1–8 de 47
| Persona | Rol | Estado | Reservas | Registro |
|---|---|---|---|---|
| Ana GarcíaNuevo, ana.garcia@correo.mx | Cliente | Activo | 14 | |
| Iván López, ivan@lunaestudio.mx | Equipo | Activo | 0 | |
| Mariana PeñaNuevo, mariana.pena@correo.mx | Cliente | Activo | 1 | |
| Sofía Ruiz, sofia@lunaestudio.mx | Equipo | Activo | 0 | |
| Diego Hernández, diego.hdz@correo.mx | Cliente | Inactivo | 23 | |
| Lucía TorresNuevo, lucia.torres@correo.mx | Cliente | Activo | 2 | |
| Regina Castro, regina@lunaestudio.mx | Admin | Activo | 0 | |
| Tomás NavarroNuevo, tomas.navarro@correo.mx | Cliente | Activo | 8 | |
Nadie coincidePrueba con otro nombre, correo o rol. |
||||
Mostrando 1–8 de 47
app/views/… (la demo de arriba)
<%# Personas de ejemplo (Ui::Demo::Person): la tabla recibe cualquier lista de
objetos, sin consultas. En tu app: rows: @people y href_for: ->(person) { person_path(person) }. %>
<% now = Time.current %>
<% roles = { "client" => "Cliente", "staff" => "Equipo", "admin" => "Admin" } %>
<% people = [
[ "Ana", "García", "ana.garcia@correo.mx", "client", true, 2.hours, 14 ],
[ "Iván", "López", "ivan@lunaestudio.mx", "staff", true, 240.days, 0 ],
[ "Mariana", "Peña", "mariana.pena@correo.mx", "client", true, 5.hours, 1 ],
[ "Sofía", "Ruiz", "sofia@lunaestudio.mx", "staff", true, 310.days, 0 ],
[ "Diego", "Hernández", "diego.hdz@correo.mx", "client", false, 40.days, 23 ],
[ "Lucía", "Torres", "lucia.torres@correo.mx", "client", true, 1.day, 2 ],
[ "Regina", "Castro", "regina@lunaestudio.mx", "admin", true, 420.days, 0 ],
[ "Tomás", "Navarro", "tomas.navarro@correo.mx", "client", true, 6.days, 8 ]
].map.with_index(1) do |(first, last, email, role, active, ago, bookings), id|
Ui::Demo::Person.new(id:, first_name: first, last_name: last, email:, role:, active:, created_at: now - ago, bookings:)
end %>
<% counts = people.map(&:role).tally %>
<div class="w-full max-w-4xl">
<%= render Ui::DataTableComponent.new(rows: people, caption: "Personas del estudio",
href_for: ->(person) { "#data-table-persona-#{person.id}" },
row_data: ->(person) { { role: person.role } }) do |t| %>
<% t.with_toolbar do %>
<%= render Ui::SegmentedControlComponent.new(label: "Filtrar por rol", selected: "all",
data: { action: "segmented-control:change->data-table#filter", data_table_key_param: "role" }) do |seg| %>
<% seg.with_option(label: "Todos", value: "all", count: people.size) %>
<% seg.with_option(label: "Clientes", value: "client", count: counts["client"]) %>
<% seg.with_option(label: "Equipo", value: "staff", count: counts["staff"]) %>
<% seg.with_option(label: "Admins", value: "admin", count: counts["admin"]) %>
<% end %>
<%= t.search(placeholder: "Buscar por nombre o correo…", label: "Buscar personas") %>
<% end %>
<% t.with_column("Persona") { |person| t.person(person, badge: ("Nuevo" if person.recent?)) } %>
<% t.with_column("Rol", hide_below: :md) { |person| roles[person.role] } %>
<% t.with_column("Estado") { |person| t.status(person.active? ? "active" : "inactive", label: person.active? ? "Activo" : "Inactivo", tone: (:danger unless person.active?)) } %>
<% t.with_column("Reservas", numeric: true, hide_below: :lg) { |person| person.bookings } %>
<% t.with_column("Registro", align: :right, hide_below: :sm) { |person| t.date(person.created_at) } %>
<% t.with_empty do %>
<%= render Ui::EmptyStateComponent.new(title: "Nadie coincide", description: "Prueba con otro nombre, correo o rol.") %>
<% end %>
<% t.with_footer do %>
<p class="tabular-nums">Mostrando <span class="text-foreground">1–8</span> de <span class="text-foreground">47</span></p>
<nav class="flex items-center gap-1" aria-label="Paginación">
<%= render Ui::ButtonComponent.new(variant: :outline, size: :sm, disabled: true, "aria-label": "Página anterior") do %>Anterior<% end %>
<%= render Ui::ButtonComponent.new(variant: :outline, size: :sm, href: "#data-table-pagina-2") do %>Siguiente<% end %>
</nav>
<% end %>
<% end %>
</div>Qué hace
- Fila clicable que respeta Cmd/Ctrl, clic medio, selección de texto y los botones de la fila.
- Filtro del cliente para listas cortas — texto sin acentos y control segmentado combinados.
- Orillas que se desvanecen hacia donde hay más tabla al desplazarse en horizontal.
- Columnas que se esconden según el ancho de la propia tabla (container queries).
Props
| Nombre | Tipo | Default | Descripción |
|---|---|---|---|
| rows / caption | Enumerable / String | — | Las filas (registros reales) y el nombre de la tabla para lectores de pantalla. |
| with_column(header, align:, numeric:, hide_below:, width:) | bloque |fila| | — | Una columna; el bloque recibe la fila. numeric: alinea a la derecha con cifras tabulares; hide_below: :sm/:md/:lg la esconde si la tabla es más angosta. |
| href_for | ->(fila) { url } | nil | Fila clicable: enlace real en la primera celda y clic en toda la fila (row_href: es su nombre anterior). |
| row_data / row_id | ->(fila) { … } | nil | Atributos data (para filtrar en el cliente) e id de cada fila (para Turbo Streams). |
| density | :compact | :regular | :relaxed | :regular | Aire de las celdas. |
| show_caption | Boolean | false | Muestra el caption arriba de la tabla; si no, queda solo para lectores. |
| max_height | String (CSS) | nil | Alto máximo con scroll propio; el encabezado se queda fijo arriba. |
| with_toolbar / with_footer / with_empty | slots | — | Búsqueda y filtros, paginación, y el estado vacío (por defecto «Sin resultados»). |
| t.person / t.status / t.date / t.money / t.search | celdas de ayuda | — | Avatar + nombre + correo, pastilla de estado :xs, fecha relativa o absoluta, $1,350 tabular y la búsqueda de la barra. |
Accesibilidad
- <table> nativa con <caption> y encabezados scope=col.
- El enlace de la fila es un <a> real en la primera celda (Tab, Enter, lector de pantalla).
- El conteo de resultados al filtrar se anuncia con aria-live.
- Con scroll propio, la caja es una región con nombre que se recorre con el teclado.