Moodinary/UI

Action row

Listas de acciones. Estado a la izquierda, impacto a la derecha; “Midiendo…” hasta tener 2 semanas.

Acciones con título, contexto y resultado. ActionRow es la fila compacta del rail; ActionListItem admite equipo, fecha, nota, check y menú. El impacto se pasa ya calculado: el componente no determina cuándo una medición es válida.

Instalación

shadcn
npx shadcn@latest add https://design.moodinary.com/r/action-list.json
import
import { ActionList, ActionListItem, ImpactValue } from "@moodinary/ui/components/action-list";
import { ActionRow } from "@moodinary/ui/components/rail-row";

Ejemplos

Plan de acción

Como en Acciones: una activa con vencimiento y menú Finalizar y medir, una cerrada con su impacto (antes → después) y una cancelada. Al finalizar la activa aparece la nota de datos insuficientes.

  • Charla con 5.º B sobre la semana de pruebasRonda corta en tutoría para hablar de cómo llegan.
    Vence jue 8 octMidiendo…
  • Recreo extendido los viernes3,0 → 3,6 en 2 semanas
    Cerrada vie 11 sep+0,6
  • Reordenar turnos de guardiaCancelada · no se mide su impacto
    Cerrada sáb 5 sep
Código
list.tsx
import {
	ActionList,
	ActionListItem,
	ImpactValue,
} from "@moodinary/ui/components/action-list";
import { Button } from "@moodinary/ui/components/button";
import { ConfirmDialog } from "@moodinary/ui/components/confirm-dialog";
import {
	DropdownMenu,
	DropdownMenuContent,
	DropdownMenuItem,
	DropdownMenuTrigger,
} from "@moodinary/ui/components/dropdown-menu";
import { MoreHorizontalIcon } from "@moodinary/ui/components/icons";
import { useState } from "react";

/*
 * Igual que ActionRow en apps/pulses/.../action-plan-widget.tsx: "Vence"/"Cerrada", detalle con
 * descripción o antes → después, menú Finalizar y medir / Cancelar solo en activas. Sin red.
 */
function actionDetail(
	status: "active" | "done" | "cancelled",
	done: boolean
): string | null {
	if (status === "cancelled") {
		return "Cancelada · no se mide su impacto";
	}
	return done ? null : "Ronda corta en tutoría para hablar de cómo llegan.";
}

export default function ActionRowList() {
	const [status, setStatus] = useState<"active" | "done" | "cancelled">(
		"active"
	);
	const [confirm, setConfirm] = useState<"done" | "cancel" | null>(null);
	const done = status !== "active";
	const title = "Charla con 5.º B sobre la semana de pruebas";
	return (
		<>
			<ActionList className="w-full">
				<ActionListItem
					actions={
						done ? null : (
							<DropdownMenu>
								<DropdownMenuTrigger
									render={
										<Button
											aria-label={`Opciones de ${title}`}
											size="icon-sm"
											variant="ghost"
										/>
									}
								>
									<MoreHorizontalIcon />
								</DropdownMenuTrigger>
								<DropdownMenuContent align="end">
									<DropdownMenuItem onClick={() => setConfirm("done")}>
										Finalizar y medir
									</DropdownMenuItem>
									<DropdownMenuItem onClick={() => setConfirm("cancel")}>
										Cancelar
									</DropdownMenuItem>
								</DropdownMenuContent>
							</DropdownMenu>
						)
					}
					checkLabel={`Marcar como finalizada: ${title}`}
					date={done ? "Cerrada mar 6 oct" : "Vence jue 8 oct"}
					detail={actionDetail(status, done)}
					done={done}
					impact={
						done ? null : <ImpactValue className="max-sm:hidden" value={null} />
					}
					note={
						status === "done"
							? "Todavía no hay datos suficientes para medir: 4 pulsos antes y 3 después (hacen falta 10 en cada período)."
							: null
					}
					onCheck={done ? undefined : () => setConfirm("done")}
					team="5.º B"
					title={title}
				/>
				<ActionListItem
					checkLabel="Marcar como finalizada: Recreo extendido los viernes"
					date="Cerrada vie 11 sep"
					detail="3,0 → 3,6 en 2 semanas"
					done
					impact={<ImpactValue value={0.6} />}
					team="4.º A"
					title="Recreo extendido los viernes"
				/>
				<ActionListItem
					checkLabel="Marcar como finalizada: Reordenar turnos de guardia"
					date="Cerrada sáb 5 sep"
					detail="Cancelada · no se mide su impacto"
					done
					team="Operaciones"
					title="Reordenar turnos de guardia"
				/>
			</ActionList>
			<ConfirmDialog
				confirmLabel={confirm === "done" ? "Finalizar" : "Cancelar acción"}
				confirmVariant={confirm === "done" ? "primary" : "danger"}
				description={
					confirm === "done"
						? `«${title}». Vamos a marcarla como terminada y a medir su impacto comparando el clima de antes y después.`
						: `«${title}». Va a quedar registrada como cancelada y no se va a medir su impacto.`
				}
				onConfirm={() => {
					setStatus(confirm === "done" ? "done" : "cancelled");
					setConfirm(null);
				}}
				onOpenChange={(open) => {
					if (!open) {
						setConfirm(null);
					}
				}}
				open={confirm !== null}
				pendingLabel="Guardando…"
				title={
					confirm === "done"
						? "¿Finalizar esta acción?"
						: "¿Cancelar esta acción?"
				}
			/>
		</>
	);
}

En el rail

Acciones abiertas en Inicio: hasta tres, con la fecha de cierre como meta.

Acciones abiertas

Ver todas →
  • Charla con 5.º B sobre la semana de pruebasjue, 8 oct
  • Revisar la carga de Atención al clientejue, 15 oct
  • Reunión con familias de 2.º B
Código
compact.tsx
import { RailCard } from "@moodinary/ui/components/rail-card";
import { ActionRow } from "@moodinary/ui/components/rail-row";

/* Igual que OpenActions en apps/pulses/.../inicio-rail.tsx: hasta 3 acciones abiertas, meta = fecha de cierre. */
const fmt = (d: string) =>
	new Date(`${d}T12:00:00`).toLocaleDateString("es-AR", {
		day: "numeric",
		month: "short",
		weekday: "short",
	});

export default function ActionRowCompact() {
	return (
		<RailCard
			action={<a href="#acciones">Ver todas →</a>}
			className="w-[340px] max-w-full"
			title="Acciones abiertas"
		>
			<ul className="m-0 grid list-none gap-2.5 p-0 text-sm">
				<ActionRow
					meta={fmt("2026-10-08")}
					title="Charla con 5.º B sobre la semana de pruebas"
				/>
				<ActionRow
					meta={fmt("2026-10-15")}
					title="Revisar la carga de Atención al cliente"
				/>
				<ActionRow title="Reunión con familias de 2.º B" />
			</ul>
		</RailCard>
	);
}

Props

ActionList

PropTipoDefaultDescripción
childrenReactNode-Filas ActionListItem dentro de una lista ul.
classNamestring-Clases del ul, no de la Card exterior.

ActionListItem

PropTipoDefaultDescripción
titleReactNode-Nombre obligatorio de la acción.
detailReactNode-Contexto o resultado en la segunda línea.
teamReactNode-Equipo asociado, visible en escritorio.
dateReactNode-Fecha o período ya formateado.
doneboolean-Marca el check como completado.
onCheck() => void-Convierte el check decorativo en botón.
checkLabelstring-Nombre accesible obligatorio cuando el check es interactivo.
checkDisabledboolean-Bloquea el botón del check.
impactReactNode-Resultado, normalmente ImpactValue.
actionsReactNode-Menú o controles de la fila.
noteReactNode-Aclaración al pie de la fila.

ImpactValue

PropTipoDefaultDescripción
valuenumber | null-Delta, con signo y un decimal. null muestra el estado pendiente.
pendingLabelReactNode"Midiendo…"Texto cuando todavía no hay medición.

ActionRow

PropTipoDefaultDescripción
titleReactNode-Título de la fila compacta.
metaReactNode-Información secundaria a la derecha.

Cuándo sí y cuándo no

Hacé
  • Diferenciá pendiente de un impacto de cero.
  • Nombrá responsable y período cuando existan.
  • Dale nombre específico al check y al menú.
Evitá
  • No inventes impacto cuando falta la medición.
  • No atribuyas causalidad a una comparación simple.
  • No uses un círculo sin texto como único estado.

Accesibilidad

  • ActionList y sus items forman una lista ul/li; ActionRow también necesita una lista.
  • El check interactivo se enfoca con Tab y se activa con Espacio o Enter.
  • El menú de opciones debe permitir flechas y Escape, y devolver el foco al disparador.