Moodinary/UI

Rail cards

Columna derecha del inicio (320 px). Listas cortas con “Ver todo”.

Tarjetas compactas para la columna secundaria del inicio. Un título, pocas filas y una salida a la vista completa. El contenido puede ser clima por equipo o próximos eventos de cualquier organización.

Instalación

shadcn
npx shadcn@latest add https://design.moodinary.com/r/rail-card.json
import
import { RailCard } from "@moodinary/ui/components/rail-card";
import { EventRow } from "@moodinary/ui/components/rail-row";

Ejemplos

Clima por equipo

La fila real de Inicio: punto de ánimo, equipo, nivel, promedio y flecha de tendencia. Cada fila lleva a los pulsos de ese equipo; sin datos muestra un punto gris.

Código
teams.tsx
import { MOOD_NAMES, MoodDot } from "@moodinary/ui/components/mood-dot";
import { RailCard } from "@moodinary/ui/components/rail-card";

/* Igual que TeamClimateRow en apps/pulses/.../inicio-rail.tsx: punto, nombre, nivel, promedio y tendencia. */
/* clampLevel de apps/pulses/src/lib/mood.ts: redondeo al nivel más cercano. */
const clampLevel = (avg: number) =>
	Math.max(1, Math.min(5, Math.round(avg))) as 1 | 2 | 3 | 4 | 5;

const teams: { name: string; avg: number | null; delta: number | null }[] = [
	{ avg: 3.6, delta: 0.2, name: "4.º A" },
	{ avg: 3.3, delta: 0, name: "Administración" },
	{ avg: 3.0, delta: -0.1, name: "6.º A" },
	{ avg: 2.4, delta: -0.6, name: "5.º B" },
	{ avg: null, delta: null, name: "Mantenimiento" },
];

function Trend({ delta }: { delta: number | null }) {
	if (delta === null || Math.abs(delta) < 0.1) {
		return (
			<span aria-hidden className="w-3 text-muted-foreground text-xs">
				→
			</span>
		);
	}
	return (
		<span
			className={
				delta > 0 ? "w-3 text-success text-xs" : "w-3 text-destructive text-xs"
			}
		>
			<span aria-hidden>{delta > 0 ? "↑" : "↓"}</span>
			<span className="sr-only">{delta > 0 ? "sube" : "baja"}</span>
		</span>
	);
}

export default function RailTeams() {
	return (
		<RailCard
			action={<a href="#analiticas">Ver analíticas →</a>}
			className="w-[340px] max-w-full"
			title="Clima por equipo"
		>
			<ul className="m-0 grid list-none gap-2.5 p-0 text-sm">
				{teams.map(({ name, avg, delta }) => (
					<li key={name}>
						<a
							className="-mx-1.5 block rounded-[7px] px-1.5 py-0.5 text-ink no-underline outline-none transition-colors hover:bg-bg-soft focus-visible:ring-3 focus-visible:ring-org-ring"
							href="#pulsos"
							title={`Ver pulsos de ${name}`}
						>
							<span className="flex items-center gap-2.5">
								{avg === null ? (
									<span
										aria-hidden
										className="size-2 shrink-0 rounded-full bg-line"
									/>
								) : (
									<MoodDot className="mr-0" level={clampLevel(avg)} />
								)}
								<span className="min-w-0 flex-1 truncate">{name}</span>
								{avg === null ? (
									<span className="text-muted-foreground text-xs">
										sin datos
									</span>
								) : (
									<>
										<span className="text-muted-foreground text-xs">
											{MOOD_NAMES[clampLevel(avg) - 1]}
										</span>
										<span className="w-8 text-right font-medium tabular-nums">
											{avg.toLocaleString("es-AR", {
												maximumFractionDigits: 1,
												minimumFractionDigits: 1,
											})}
										</span>
										<Trend delta={delta} />
									</>
								)}
							</span>
						</a>
					</li>
				))}
			</ul>
			<p className="m-0 text-ink-3 text-xs">
				Muy bajo &lt;1,8 · Bajo &lt;2,6 · Neutral &lt;3,4 · Bien &lt;4,2 · Muy
				bien
			</p>
		</RailCard>
	);
}

Próximos eventos

Hasta tres eventos del calendario con su fecha. Los de día completo se formatean en UTC.

Próximos eventos

Ver calendario →
  • 9octJornada deportivavie
  • 14octCierre de mes de facturaciónmié
  • 16octCierre de trimestrevie
Código
events.tsx
import { RailCard } from "@moodinary/ui/components/rail-card";
import { EventRow } from "@moodinary/ui/components/rail-row";

/* Igual que UpcomingEvents en apps/pulses/.../inicio-rail.tsx: hasta 3 eventos, título simple. */
const events = [
	{ date: "2026-10-09", title: "Jornada deportiva" },
	{ date: "2026-10-14", title: "Cierre de mes de facturación" },
	{ date: "2026-10-16", title: "Cierre de trimestre" },
];

export default function RailEvents() {
	return (
		<RailCard
			action={<a href="#calendario">Ver calendario →</a>}
			className="w-[340px] max-w-full"
			title="Próximos eventos"
		>
			<ul className="m-0 grid list-none gap-2.5 p-0 text-sm">
				{events.map(({ date, title }) => (
					<EventRow
						date={new Date(date)}
						key={title}
						timeZone="UTC"
						title={title}
					/>
				))}
			</ul>
		</RailCard>
	);
}

Props

RailCard

PropTipoDefaultDescripción
titleReactNode-Título obligatorio, renderizado como h2.
actionReactNode-Enlace o acción del encabezado.
childrenReactNode-Lista o contenido compacto.
classNamestring-Clases de la tarjeta; el ancho lo decide la composición.

EventRow

PropTipoDefaultDescripción
dateDate-Fecha del evento.
titleReactNode-Nombre del evento.
timeZonestring-Zona para formatear fecha y día. UTC es útil para fechas de día completo.
classNamestring-Clases de la fila li.

Cuándo sí y cuándo no

Hacé
  • Usá listas cortas que se puedan leer de un vistazo.
  • Enlazá la vista completa con un nombre concreto.
  • Mantené el rail secundario respecto del contenido principal.
Evitá
  • No metas tablas completas en la columna lateral.
  • No uses el color sin una palabra o un valor.
  • No dejes una salida Ver todo que no lleve a ningún lado.

Accesibilidad

  • EventRow renderiza li: envolvelo en ul u ol.
  • El enlace del encabezado entra por Tab; las filas de lectura no deben robar foco.
  • Nombrá las barras de ánimo con su valor o mantenelas decorativas si ese valor ya está escrito.