Organism
Timeline
Requiere <UIProvider> — Primeros pasos →
Instalación
Instala solo este componente (copia el código fuente a tu proyecto, sin dependencia npm)
npx orn-ui add timelineO instala el paquete completo e impórtalo
pnpm add orn-ui
import { Timeline } from 'orn-ui/timeline';Uso
- cuándo usarlo — Hitos a lo largo de un camino: un pedido que se entrega, un roadmap, los pasos por los que pasa un cambio. Para un formulario partido en pasos está Wizard, y para un indicador de progreso pelado, Steps — éste es para un recorrido que vale la pena mirar.
- items / status — Cada hito está cumplido o pendiente. El progreso se cuenta desde el arranque y se corta en el primer pendiente: un hito cumplido detrás de uno pendiente no tira de la línea, porque un camino es un camino.
- advance — Qué puede hacer un toque. 'free' deja que responda cualquier hito y que la línea siga a items para donde vaya; 'sequential' sólo responde el que sigue a la línea y nunca retrocede; 'revisit' responde además todo lo ya recorrido, y tampoco retrocede.
- selectedIndex — Hasta dónde llegaste y qué estás mirando no son lo mismo. Éste marca con un borde el hito que se está leyendo, y puede quedar detrás de la línea sin tirarla para atrás.
- curve / startSide / spacing — La forma del camino. curve={0} lo dibuja recto, startSide da vuelta hacia qué lado sale la primera píldora, y spacing es el aire que recibe cada hito. Dejale lugar al halo — sobresale unos 20px de la píldora.
- glow — El halo son muchas capas tenues y no tres fuertes: con pocos escalones se ven los anillos, que es justo lo contrario de un difuminado.
Clip del demo todavía sin grabar — ver MEDIA.md
Variantes
default — the line bows, the pills alternate sides
<Timeline items={ROADMAP} />curve={0} — a straight spine
<Timeline items={PIPELINE} curve={0} />startSide="left" and a wider curve
<Timeline items={PIPELINE} startSide="left" curve={44} />glow={false} — just the pills
<Timeline items={ROADMAP} glow={false} />spacing={60} — tighter, inside a Card
<Card>
<Timeline items={PIPELINE} spacing={60} curve={20} />
</Card>a single item — no line to draw
<View>
<Timeline items={[{ label: 'Atomic design', emoji: '⚛️' }]} />
</View>Código completo del demo
function upTo(items: TimelineItem[], reached: number): TimelineItem[] {
return items.map((item, index) => ({ ...item, status: index <= reached ? undefined : 'pending' }));
}
/**
* Tocar un hito lo alcanza, y con él todo lo anterior: la línea viaja hasta
* ahí. Tocar el que ya está alcanzado da marcha atrás, para poder mirar la
* animación en los dos sentidos sin recargar.
*/
function TappableTimeline() {
const [reached, setReached] = useState(1);
return (
<View style={{ gap: 8 }}>
<Timeline
items={upTo(ROADMAP, reached)}
onItemPress={(index) => setReached(index === reached ? index - 1 : index)}
/>
{/* El eco existe para el flow de Maestro: el nodo que se llena y la
línea que viaja son píxeles, y un assert no los ve. El texto sí. */}
<Body>Reached: {ROADMAP[reached]?.label ?? 'nothing yet'}</Body>
</View>
);
}
/**
* Avanza solo y vuelve a empezar: el demo existe para mirar el movimiento.
*
* Sin botón de pausa a propósito. Iba uno, y caía justo donde el pager de
* VariantList arranca su swipe (80% de la pantalla): el botón se quedaba con
* el gesto y no se podía pasar de variante.
*/
function AutoTimeline() {
const [reached, setReached] = useState(-1);
useEffect(() => {
const id = setInterval(() => {
setReached((previous) => (previous >= ROADMAP.length - 1 ? -1 : previous + 1));
}, 1100);
return () => clearInterval(id);
}, []);
return (
<View style={{ gap: 8 }}>
<Timeline items={upTo(ROADMAP, reached)} duration={700} />
<Body>Now at: {ROADMAP[reached]?.label ?? 'the start'}</Body>
</View>
);
}
/**
* Avance estricto: sólo responde el hito que sigue, y lo alcanzado no se
* devuelve. Es el onboarding que no deja saltear pasos.
*/
function SequentialTimeline() {
const [reached, setReached] = useState(0);
return (
<View style={{ gap: 8 }}>
<Timeline
items={upTo(ROADMAP, reached)}
advance="sequential"
onItemPress={setReached}
/>
<Body>
{reached >= ROADMAP.length - 1
? 'All done — nothing left to tap'
: `Only "${ROADMAP[reached + 1]?.label}" answers`}
</Body>
</View>
);
}
/**
* Progreso guardado, contenido revisitable: tocar un hito anterior cambia lo
* que estoy mirando, no lo que llevo recorrido. La línea se queda donde
* llegó y el borde marca dónde estoy parado.
*/
function RevisitTimeline() {
const [reached, setReached] = useState(2);
const [looking, setLooking] = useState(2);
return (
<View style={{ gap: 8 }}>
<Timeline
items={upTo(ROADMAP, reached)}
advance="revisit"
selectedIndex={looking}
onItemPress={(index) => {
setLooking(index);
// Sólo el hito que sigue empuja el recorrido; los de atrás son una
// visita.
if (index > reached) setReached(index);
}}
/>
<Body>Looking at: {ROADMAP[looking]?.label}</Body>
<Body>Progress stays at: {ROADMAP[reached]?.label}</Body>
</View>
);
}
export function TimelineDemo() {
const variants: VariantDef[] = [
{
label: 'default — the line bows, the pills alternate sides',
content: <Timeline items={ROADMAP} />,
},
{
label: 'onItemPress — tap a milestone and the line travels there',
content: <TappableTimeline />,
},
{
label: 'advance="sequential" — only the next milestone answers, no going back',
content: <SequentialTimeline />,
},
{
label: 'advance="revisit" — revisit the past, keep the progress',
content: <RevisitTimeline />,
},
{
label: 'advancing on its own — the line draws itself, gap by gap',
content: <AutoTimeline />,
},
{
label: 'curve={0} — a straight spine',
content: <Timeline items={PIPELINE} curve={0} />,
},
{
label: 'startSide="left" and a wider curve',
content: <Timeline items={PIPELINE} startSide="left" curve={44} />,
},
{
label: 'glow={false} — just the pills',
content: <Timeline items={ROADMAP} glow={false} />,
},
{
label: 'spacing={60} — tighter, inside a Card',
content: (
<Card>
<Timeline items={PIPELINE} spacing={60} curve={20} />
</Card>
),
},
{
label: 'a single item — no line to draw',
content: (
<View>
<Timeline items={[{ label: 'Atomic design', emoji: '⚛️' }]} />
</View>
),
},
];
return <VariantList variants={variants} />;Props
Además acepta testID, que se reenvía al nodo raíz. Existe para los tests end-to-end (Maestro maneja la app por el árbol de accesibilidad) y no cambia en nada cómo se ve ni cómo se comporta el componente.
| Nombre | Tipo | Por defecto | Descripción |
|---|---|---|---|
items | TimelineItem[] | — | Los hitos, en orden. Cada uno lleva una etiqueta, opcionalmente un ícono o un emoji, y su estado. |
curve? | number | 28 | Píxeles que la línea se arquea respecto del camino recto. 0 la dibuja derecha. |
spacing? | number | 84 | Distancia vertical entre un nodo y el siguiente. |
startSide? | 'left' | 'right' | right | Lado hacia el que sale la primera píldora; las demás alternan. |
glow? | boolean | true | Halo detrás de cada píldora. |
onItemPress? | (index: number) => void | — | Hace tocable cada píldora y avisa su índice. |
advance? | 'free' | 'sequential' | 'revisit' | free | Qué toques se permiten, y si la línea puede volver atrás. |
selectedIndex? | number | — | El hito que se está mirando ahora, resaltado. No es el progreso: puede quedar detrás de la línea sin tirarla para atrás. |
duration? | number | 420 | Milisegundos que tarda la línea en recorrer un vano cuando se alcanza un hito. |
style? | StyleProp<ViewStyle> | — | Estilo del contenedor. |