Planificación
Las cinco vistas para planificar y seguir trabajo: el calendario, el tablero, el diagrama de barras, el historial y el asistente por pasos.
Los cuatro primeros comparten dos convenciones:
- Son de solo lectura hasta que les das el callback de cambio. Sin
onEventChange,onCardMovenionTaskChangeno se puede arrastrar nada, que es lo que quieres en una vista de consulta. - El «hoy» es fijable.
todayenCalendaryGantt,nowenTimeline: así una captura o una prueba no dependen del reloj de quien la ejecuta.
Calendar
Sección titulada «Calendar»Calendario de semana y de mes, con eventos arrastrables y una cola de tarjetas sin programar.
import { Calendar, type CalendarEvent } from '@openfactu/ui';
const EVENTOS: CalendarEvent[] = [ { id: 'hormigon', title: 'Llevar hormigón', start: '2026-07-22T07:00:00', end: '2026-07-22T08:00:00', status: 'en-curso', meta: 'A. Pérez' }, { id: 'revision', title: 'Revisión de facturas', start: '2026-07-21T09:00:00', end: '2026-07-21T11:00:00', status: 'hecha' }, { id: 'visita', title: 'Visita de obra', start: '2026-07-23T16:30:00', end: '2026-07-23T18:00:00', status: 'pendiente', meta: 'Con el cliente' }, { id: 'inventario', title: 'Cierre por inventario', start: '2026-07-25T00:00:00', end: '2026-07-25T23:59:00', allDay: true, status: 'bloqueada' },];
<Calendar events={EVENTOS} aria-label="Calendario de tareas" />El color del bloque sale de status —pendiente, en-curso, hecha,
bloqueada—, y color lo pisa si necesitas uno propio. Los eventos con allDay van a
la banda superior, sin hora.
Arrastrar, crear y programar
Sección titulada «Arrastrar, crear y programar»<Calendar events={eventos} onEventChange={(evento, { start, end }) => reprogramar(evento.id, start, end)} onEventClick={abrirFicha} onSlotClick={(inicio) => crearRapido(inicio)} unscheduled={sinFecha} onSchedule={(evento, inicio) => programar(evento.id, inicio)} unscheduledLabel="Pendientes de asignar"/>unscheduled pinta una cola lateral de tarjetas sin fecha: al arrastrar una al
calendario se llama a onSchedule. Es el flujo de asignar partes de trabajo a un día.
Un evento con locked ni se mueve ni se redimensiona, aunque el calendario sea
editable.
Vista y fecha
Sección titulada «Vista y fecha»<Calendar events={eventos} view={vista} onViewChange={setVista} date={dia} onDateChange={setDia} hourStart={7} hourEnd={20} hourHeight={44}/>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
events |
CalendarEvent[] |
— | Eventos |
view |
'week' | 'month' |
'week' |
Vista |
onViewChange |
(v: CalendarView) => void |
— | Cambio de vista |
date |
Date | string |
hoy | Día de referencia: define la semana o el mes visible |
onDateChange |
(d: Date) => void |
— | Cambio de fecha visible |
toolbar |
boolean |
true |
Barra con «‹ Hoy ›» y el selector de vista |
hourStart / hourEnd |
number |
6 / 22 |
Primera y última hora de la rejilla |
hourHeight |
number |
36 |
Alto en píxeles de cada hora |
today |
Date | string |
el reloj | Qué día se considera hoy |
onEventChange |
(e: CalendarEvent, c: CalendarChange) => void |
— | Mover o cambiar duración. Sin esto es solo lectura |
onEventClick |
(e: CalendarEvent) => void |
— | Click en un evento |
onSlotClick |
(start: Date) => void |
— | Click en un hueco vacío: crear rápido |
unscheduled |
CalendarEvent[] |
— | Cola de tarjetas sin fecha |
onSchedule |
(e: CalendarEvent, start: Date) => void |
— | Se arrastra una tarjeta al calendario |
unscheduledLabel |
ReactNode |
'Sin programar' |
Título de la cola |
maxPorDia |
number |
3 |
Eventos por casilla del mes antes del «+N más» |
emptyMessage |
ReactNode |
'No hay nada en estas fechas.' |
Estado vacío |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
CalendarEvent
Sección titulada «CalendarEvent»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
title |
string |
Título del bloque |
start / end |
Date | string |
Inicio y fin |
allDay |
boolean |
Va a la banda superior, sin hora |
status |
'pendiente' | 'en-curso' | 'hecha' | 'bloqueada' |
Decide el color |
color |
string |
Color propio; manda sobre el del estado |
textColor |
string |
Texto sobre color |
locked |
boolean |
Ni se mueve ni se redimensiona |
meta |
ReactNode |
Segunda línea dentro del bloque |
Tablero de columnas con tarjetas arrastrables y límite de trabajo en curso.
import { Kanban, Badge, Avatar, type KanbanColumn } from '@openfactu/ui';
const COLUMNAS: KanbanColumn[] = [ { id: 'por-hacer', label: 'Por hacer', cards: [ { id: 't1', title: 'Revisar facturas de agosto', description: 'Cuadrar con el extracto bancario antes del cierre.', badges: <Badge variant="warning">Vence hoy</Badge>, meta: <Avatar name="Ana Pérez" size="xs" />, tone: 'warning', }, { id: 't2', title: 'Pedir presupuesto de grúa', badges: <Badge variant="neutral">Compras</Badge> }, ], }, { id: 'en-curso', label: 'En curso', tone: 'accent', wipLimit: 2, cards: [ { id: 't3', title: 'Alta de proveedor Talleres del Norte', description: 'Falta el certificado de estar al corriente.', tone: 'accent', meta: <Avatar name="Bruno Gil" size="xs" />, }, ], }, { id: 'bloqueadas', label: 'Bloqueadas', tone: 'danger', cards: [] },];
<Kanban columns={COLUMNAS} aria-label="Tablero de tareas" />wipLimit es un aviso, no una prohibición: al pasarse, el contador de la columna lo
señala, pero no impide soltar nada.
Mover tarjetas
Sección titulada «Mover tarjetas»<Kanban columns={columnas} onCardMove={(tarjeta, destino, origen) => { setColumnas((prev) => { const copia = estructuraProfunda(prev); copia[indice(origen.columnId)].cards.splice(origen.index, 1); copia[indice(destino.columnId)].cards.splice(destino.index, 0, tarjeta); return copia; }); }} onCardClick={abrirTarea}/>Una tarjeta con locked no se arrastra; una columna con locked no acepta que le
suelten nada.
Tarjeta propia
Sección titulada «Tarjeta propia»renderCard sustituye el pintado conservando el arrastre:
<Kanban columns={columnas} onCardMove={mover} renderCard={(tarjeta, columna) => <MiTarjeta tarea={tarjeta} columna={columna.id} />}/>| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
columns |
KanbanColumn[] |
— | Columnas con sus tarjetas |
onCardMove |
(card, to: KanbanMove, from: KanbanMove) => void |
— | Arrastrar y soltar. Sin esto es solo lectura |
onCardClick |
(card: KanbanCard) => void |
— | Click en una tarjeta |
renderCard |
(card, column) => ReactNode |
— | Sustituye el pintado de la tarjeta |
columnWidth |
number |
260 |
Ancho de cada columna en píxeles |
maxHeight |
number | string |
— | Alto máximo de la lista; a partir de ahí, scroll propio |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
KanbanColumn
Sección titulada «KanbanColumn»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
label |
ReactNode |
Título de la columna |
cards |
KanbanCard[] |
Tarjetas |
wipLimit |
number |
Máximo recomendado; avisa al pasarse |
tone |
KanbanTone |
Color del encabezado |
locked |
boolean |
No acepta que le suelten nada |
emptyMessage |
ReactNode |
Qué poner con la columna vacía |
KanbanCard
Sección titulada «KanbanCard»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
title |
ReactNode |
Título |
description |
ReactNode |
Detalle bajo el título |
badges |
ReactNode |
Chips: etiquetas, prioridad |
meta |
ReactNode |
Bloque a la derecha: avatar, importe, fecha |
tone |
KanbanTone |
Color de la tarjeta |
locked |
boolean |
No se puede arrastrar |
KanbanMove es { columnId, index }, con index empezando en 0.
Diagrama de barras con dependencias, avance, agrupación y cuatro escalas.
import { Gantt, type GanttTask } from '@openfactu/ui';
const OBRA: GanttTask[] = [ { id: 'proyecto', name: 'Redacción del proyecto', start: '2026-07-06', end: '2026-07-17', status: 'hecha', progress: 1, group: 'Preparación', }, { id: 'licencia', name: 'Licencia de obra', start: '2026-07-20', end: '2026-08-07', status: 'en-curso', progress: 0.4, dependencies: ['proyecto'], group: 'Preparación', }, { id: 'cimentacion', name: 'Cimentación', start: '2026-08-10', end: '2026-08-28', status: 'pendiente', dependencies: ['licencia'], group: 'Ejecución', },];
<Gantt tasks={OBRA} scale="week" aria-label="Planificación de la obra" />group mete las tareas bajo una fila de encabezado —proyecto, fase, persona—;
dependencies dibuja la flecha entre barras; progress (de 0 a 1) rellena el avance
dentro de la barra.
Escala y edición
Sección titulada «Escala y edición»<Gantt tasks={tareas} scale={escala} {/* 'day' | 'week' | 'month' | 'quarter' */} rangeStart="2026-07-01" rangeEnd="2026-12-31" onTaskChange={(tarea, { start, end }) => replanificar(tarea.id, start, end)} onTaskClick={abrirTarea} labelWidth={260} rowHeight={40}/>Se combina bien con un
SegmentedControl para cambiar de escala.
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
tasks |
GanttTask[] |
— | Tareas |
scale |
'day' | 'week' | 'month' | 'quarter' |
'week' |
Escala del eje |
rangeStart / rangeEnd |
Date | string |
los de las tareas, con margen | Primer y último día visibles |
onTaskChange |
(task, change: GanttChange) => void |
— | Mover o cambiar duración. Sin esto es solo lectura |
onTaskClick |
(task: GanttTask) => void |
— | Click en una tarea |
labelWidth |
number |
220 |
Ancho de la columna de nombres |
rowHeight |
number |
34 |
Alto de cada fila |
showToday |
boolean |
true |
Marca vertical del día de hoy |
today |
Date | string |
el reloj | Qué se considera hoy |
emptyMessage |
string |
'No hay tareas que mostrar.' |
Estado vacío |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
GanttTask
Sección titulada «GanttTask»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
name |
string |
Nombre, en la columna izquierda |
start / end |
Date | string |
Inicio y fin, inclusive. Iguales = hito |
progress |
number |
0..1; relleno de avance dentro de la barra |
status |
'pendiente' | 'en-curso' | 'hecha' | 'bloqueada' |
Decide el color |
color / textColor |
string |
Color propio y su color de texto |
dependencies |
string[] |
Ids de los que depende; dibuja la flecha |
group |
string |
Fila de encabezado que la agrupa |
locked |
boolean |
Ni se mueve ni se redimensiona |
meta |
ReactNode |
Texto a la derecha de la barra |
Timeline
Sección titulada «Timeline»El historial de un documento: quién hizo qué y cuándo.
import { Timeline, type TimelineEvent } from '@openfactu/ui';import { FileText, Pencil, Send } from 'lucide-react';
const HISTORIAL: TimelineEvent[] = [ { id: '1', title: 'Factura creada', description: 'FAC/2026/0042 por 1.240,00 € a Construcciones Acme S.L.', author: 'Ana Pérez', date: '2026-07-20T09:12:00', icon: <FileText className="h-3.5 w-3.5" />, }, { id: '2', title: 'Línea modificada', description: 'Tornillos de 10 cm: 100 → 120 unidades.', author: 'Bruno Gil', date: '2026-07-20T11:40:00', icon: <Pencil className="h-3.5 w-3.5" />, }, { id: '3', title: 'Rechazada por la AEAT', description: 'Código 3002: el NIF del destinatario no consta en el censo.', author: 'Sistema', date: '2026-07-21T08:06:00', tone: 'danger', icon: <Send className="h-3.5 w-3.5" />, },];
<Timeline events={HISTORIAL} aria-label="Historial del documento" />Por defecto agrupa por día y enseña fechas relativas («hace 2 h»). content es un
bloque libre bajo el texto, para meter las diferencias de un cambio, un adjunto o un
importe.
Compacto
Sección titulada «Compacto»Una línea por evento, para paneles laterales:
<Timeline events={historial} variant="compacto" groupByDay={false} />| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
events |
TimelineEvent[] |
— | Eventos |
variant |
'detallado' | 'compacto' |
'detallado' |
compacto es una línea por evento |
groupByDay |
boolean |
true en detallado |
Encabezado por día |
relative |
boolean |
true |
Fechas relativas en lugar de la hora |
now |
Date | string |
el reloj | Qué se considera ahora |
emptyMessage |
ReactNode |
'Todavía no hay actividad.' |
Estado vacío |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
TimelineEvent
Sección titulada «TimelineEvent»| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
title |
ReactNode |
Qué pasó |
description |
ReactNode |
Detalle bajo el título |
author |
ReactNode |
Quién lo hizo |
date |
Date | string |
Cuándo |
icon |
ReactNode |
Icono del punto |
tone |
TimelineTone |
Color del punto |
content |
ReactNode |
Bloque libre bajo el texto |
onClick |
() => void |
Click en el evento |
Stepper
Sección titulada «Stepper»El asistente por pasos.
import { Stepper, type Step } from '@openfactu/ui';
const PASOS: Step[] = [ { id: 'empresa', label: 'Datos de empresa', description: 'CIF, domicilio y actividad' }, { id: 'fiscal', label: 'Configuración fiscal', description: 'Régimen y series' }, { id: 'usuarios', label: 'Usuarios', description: 'Invita a tu equipo' }, { id: 'listo', label: 'Listo' },];
<Stepper steps={PASOS} current={paso} onStepClick={(_, i) => setPaso(i)} />current es el índice del paso en curso; los anteriores se dan por hechos sin que
tengas que marcarlos.
Marcar un paso como fallido
Sección titulada «Marcar un paso como fallido»status en un paso concreto lo pisa todo, así que puedes marcar un error sin mover el
paso actual:
<Stepper current={2} steps={[ { id: 'empresa', label: 'Datos de empresa' }, { id: 'fiscal', label: 'Configuración fiscal', status: 'error' }, { id: 'usuarios', label: 'Usuarios' }, ]}/>Vertical
Sección titulada «Vertical»<Stepper steps={PASOS} current={paso} orientation="vertical" size="sm" />| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
steps |
Step[] |
— | Pasos |
current |
number |
0 |
Índice del paso en curso |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Disposición |
size |
'sm' | 'md' |
'md' |
Tamaño |
onStepClick |
(step: Step, index: number) => void |
— | Volver a un paso |
allowForward |
boolean |
false |
Deja pulsar también los pendientes |
aria-label |
string |
— | Nombre accesible |
className |
string |
— | Clases |
| Prop | Tipo | Descripción |
|---|---|---|
id |
string |
Identificador |
label |
ReactNode |
Nombre del paso |
description |
ReactNode |
Detalle bajo el nombre |
status |
'hecha' | 'actual' | 'pendiente' | 'error' |
Se deduce de current si no se indica |
icon |
ReactNode |
Icono en lugar del número |
disabled |
boolean |
No se puede ir a este paso |