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.jsonimport
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.5.º BVence jue 8 octMidiendo…
- Recreo extendido los viernes3,0 → 3,6 en 2 semanas4.º ACerrada vie 11 sep+0,6
- Reordenar turnos de guardiaCancelada · no se mide su impactoOperacionesCerrada 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
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| children | ReactNode | - | Filas ActionListItem dentro de una lista ul. |
| className | string | - | Clases del ul, no de la Card exterior. |
ActionListItem
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| title | ReactNode | - | Nombre obligatorio de la acción. |
| detail | ReactNode | - | Contexto o resultado en la segunda línea. |
| team | ReactNode | - | Equipo asociado, visible en escritorio. |
| date | ReactNode | - | Fecha o período ya formateado. |
| done | boolean | - | Marca el check como completado. |
| onCheck | () => void | - | Convierte el check decorativo en botón. |
| checkLabel | string | - | Nombre accesible obligatorio cuando el check es interactivo. |
| checkDisabled | boolean | - | Bloquea el botón del check. |
| impact | ReactNode | - | Resultado, normalmente ImpactValue. |
| actions | ReactNode | - | Menú o controles de la fila. |
| note | ReactNode | - | Aclaración al pie de la fila. |
ImpactValue
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| value | number | null | - | Delta, con signo y un decimal. null muestra el estado pendiente. |
| pendingLabel | ReactNode | "Midiendo…" | Texto cuando todavía no hay medición. |
ActionRow
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| title | ReactNode | - | Título de la fila compacta. |
| meta | ReactNode | - | 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.