Guía del core
El paquete @reelkit/core contiene la lógica del slider independiente del framework. Úsalo para crear integraciones propias o para entender la arquitectura que hay debajo.
Visión general de la arquitectura
El core sigue un patrón de controladores con funciones factoría. Sin clases: todo son objetos planos devueltos por closures. Sin dependencias. El core coordina:
- SliderController — gestión central del estado y la navegación
- GestureController — arrastre táctil y con puntero
- KeyboardController — teclas de flecha y Escape
- WheelController — rueda del ratón con debounce
createSliderController
Crea una instancia nueva del controlador del slider, que gestiona todo su estado y su comportamiento.
import { createSliderController } from '@reelkit/core';
const controller = createSliderController(
{
count: 10,
direction: 'vertical',
enableWheel: true,
transitionDuration: 300,
},
{
onAfterChange: (index) => console.log('Changed to:', index),
}
);
// Attach to DOM element
controller.attach(element);
controller.observe();Métodos del controlador
Navegación
// Go to specific index
controller.goTo(5); // instant
controller.goTo(5, true); // animated, returns Promise
// Navigate to next/previous
controller.next();
controller.prev();Ciclo de vida
// Connect to DOM element
controller.attach(element);
// Start gesture, keyboard, and wheel observation
controller.observe();
// Stop gesture, keyboard, and wheel observation
controller.unobserve();
// Detach DOM listeners (reversible — use for React effect cleanup)
controller.detach();
// Permanent teardown (use for Angular onDestroy)
controller.dispose();
// Recalculate positions
controller.adjust();
// Update size
controller.setPrimarySize(600);Actualizar el estado
// Update configuration
controller.updateConfig({
count: 20,
loop: true,
});Virtualización
El core renderiza solo 3 slides en el DOM en todo momento (el actual, el anterior y el siguiente). El extractor de rango decide qué índices entran en la ventana renderizada:
import { defaultRangeExtractor } from '@reelkit/core';
// Default: renders current ± 1 (3 DOM nodes)
const indexes = defaultRangeExtractor(currentIndex, count);
// Custom: skip hidden slides by shifting to next valid index
const hiddenSlides = new Set([2, 5]);
const skipHiddenExtractor = (current: number, count: number) => {
const result: number[] = [];
// Collect prev, current, next — skip hidden, shift forward
for (let i = current - 1, added = 0; added < 3 && i < count; i++) {
if (i >= 0 && !hiddenSlides.has(i)) {
result.push(i);
added++;
}
}
return result;
};El resultado se limita siempre a un máximo de 3 índices. Si tu extractor devuelve más, el core se queda con 3 centrados en el slide actual.
Señales
El core usa un sistema de señales ligero para la reactividad:
import { createSignal, createComputed, reaction } from '@reelkit/core';
// Create a signal
const count = createSignal(0);
// Observe changes (returns a disposer function)
const dispose = count.observe(() => console.log(count.value));
// Update value
count.value = 5;
// Create computed signal (requires a deps factory)
const doubled = createComputed(() => count.value * 2, () => [count]);
// Run side effects on signal changes
const disposeReaction = reaction(
() => [count],
() => console.log('Count changed:', count.value)
);
// Cleanup
dispose();
disposeReaction();Estado del controlador
Accede al estado reactivo a través de controller.state:
const { index, axisValue, indexes } = controller.state;
// Observe index changes (returns a disposer function)
const disposeIndex = index.observe(() => {
console.log('Current index:', index.value);
});
// Observe visible indexes for virtualization
const disposeIndexes = indexes.observe(() => {
console.log('Visible:', indexes.value);
});
// Cleanup when done
disposeIndex();
disposeIndexes();Controlador de la línea de tiempo
Crea una barra de progreso propia para cualquier elemento <video>. El controlador expone señales reactivas para la duración, el tiempo actual, los rangos cargados y el estado del arrastre, y conecta las interacciones con puntero y teclado a cualquier elemento del DOM con una sola llamada.
import { createTimelineController } from '@reelkit/core';
const timeline = createTimelineController({
onScrubStart: () => video.pause(),
onScrubEnd: () => video.play(),
});
timeline.attach(video);
const dispose = timeline.bindInteractions(trackEl);
// Render: read signals and update DOM
timeline.progress.observe(() => {
fillEl.style.width = `${timeline.progress.value * 100}%`;
});
// Cleanup
dispose();
timeline.detach();Estado en la URL
Guarda en la barra de direcciones si un overlay está abierto: el slide visible obtiene un enlace que se puede compartir, abrir directamente y cerrar con el botón de volver. El core es dueño del modelo; los bindings lo envuelven en un hook (useOverlayUrlState en React y Vue, createOverlayUrlState en Angular) y en un componente overlay controlado por la URL.
Cómo funciona
createUrlStateController refleja un parámetro de la query en una señal y escribe de vuelta los cambios. Al abrir se añade una entrada al historial; cada navegación la sustituye: cien deslizamientos no añaden ninguna, así que volver una vez siempre cierra. Un UrlAdapter es la pieza intercambiable de lectura y escritura: el de por defecto usa history.pushState, y una aplicación con router pasa un adaptador basado en el router para que su propia ubicación nunca quede desfasada.
import { createUrlStateController, urlIndexKey } from '@reelkit/core';
const controller = createUrlStateController({
param: 'photo',
...urlIndexKey(() => items.length),
});
const detach = controller.attach(); // begin mirroring the URL
controller.position.observe(() => {
// null → closed; a number → open at that slide
render(controller.position.value);
});
// Write back: opening pushes once, navigating replaces, closing clears
controller.set(3);
controller.set(null);Codec y locator: dos tareas
Una clave es un par { codec, locator } hecho a medida. Escribir una identidad en la URL y encontrar dónde está ahora son problemas distintos, así que son objetos distintos:
- codec — el formato.
encodeescribe una identidad como texto del parámetro;decodela vuelve a leer y rechaza un valor mal formado para que el parámetro se limpie solo de la URL. - locator — la búsqueda.
locateencuentra dónde está una identidad decodificada en la colección actual (onullsi ya no existe);identifyconvierte una posición en su identidad al escribir; ellocateAsyncopcional carga más páginas de un feed con ventana o infinito cuando no la encuentra.
Al mantenerlos separados puedes combinar cualquier formato con cualquier búsqueda, por ejemplo un codec de id estable con un locator que pagina.
Claves por índice o por id estable
Dos claves incluidas montan ese par por ti; solo se diferencian en lo que nombra la URL:
urlIndexKey(() => count)apunta por posición (?photo=3). Es la más sencilla, pero un marcador abre otro elemento en cuanto la lista se reordena.urlStableIdKey({ items })apunta por elidestable de cada elemento (?photo=post_42) y recorre la lista actual: el marcador sigue nombrando ese elemento tras reordenar, o se descarta limpiamente si ya no existe.hashCodec: base64UrlCodecoculta el id en base64url (es reversible, no un hash criptográfico).
¿Paginas un feed con ventana? Las dos claves incluidas aceptan un locateAsync opcional: urlIndexKey(() => count, locateAsync) y urlStableIdKey({ items, locateAsync }). La búsqueda síncrona responde con lo que ya está cargado; si no lo encuentra, carga el resto, así que un enlace compartido que queda fuera de la ventana también se abre, sin escribir un codec ni un locator a mano.
¿Dos ejes? urlIndexTwoAxisKey usa ?p=<outer>.<inner> para una publicación más el índice de un medio dentro de ella. Todas las opciones están en la referencia de la API del core.
Elementos ya vistos
Recuerda hasta dónde llegó alguien en una galería, entre recargas y entre pestañas: un anillo que muestra lo que ya se ha visto, una galería que se vuelve a abrir donde se dejó. Es el mismo modelo que el estado en la URL, pero guardado en el almacenamiento en lugar de en la barra de direcciones, así que los dos comparten la clave.
Cómo funciona
createViewedStateController guarda cada entrada con el mismo texto que llevaría un enlace ?photo= y la lee con el mismo ciclo de decode y después locate. Nunca se fía de una posición guardada. Pasa la misma clave a las dos superficies y un marcador y una entrada guardada serán la misma cadena.
import {
createUrlStateController,
createViewedStateController,
urlStableIdTwoAxisKey,
twoAxisViewedTracking,
} from '@reelkit/core';
const key = urlStableIdTwoAxisKey({ outerItems, innerItems });
const url = createUrlStateController({ param: 'story', ...key });
const seen = createViewedStateController({
storageKey: 'stories-seen',
...key,
...twoAxisViewedTracking,
});
seen.attach(); // reads storage, follows other tabs
seen.record({ outer: 2, inner: 1 }); // furthest point wins, a rewatch never rewinds
seen.resolve('user_42'); // → { outer: 2, inner: 1 } | nullLa durabilidad depende de la clave
El almacén no añade ninguna reparación propia, así que la clave que elijas decide qué sobrevive: una clave por id mantiene el punto aunque la colección se reordene, una clave por posición no. Una entrada guardada indica el punto más lejano al que se llegó, no un recuento de visualizaciones, así que quitar un elemento del medio acorta la cuenta, igual que se corrige solo un enlace compartido.
La lectura es solo síncrona. Una entrada cuyos elementos aún no se han cargado se lee como ausente y se queda intacta en el almacenamiento, así que un feed con ventana nunca borra su propio historial.
Almacenamiento y caducidad
Por defecto el almacén usa localStorage; createSessionStorageAdapter() lo olvida al cerrar la pestaña, y un StorageAdapter propio lo guarda en cualquier sitio síncrono. No se lee nada hasta attach(), así que el renderizado en el servidor y el primer renderizado en el cliente coinciden.
Las entradas se guardan hasta que se olvidan de forma explícita. Pasa ttlMs para que caduquen: por pista y con un reloj deslizante, así que algo que se sigue viendo nunca caduca junto a algo abandonado. Cambia lo que se escribe, porque cada entrada pasa a ser un par [wire, timestamp], pero la lectura admite las dos formas diga lo que diga la opción, así que una entrada guardada antes de activarla cuenta como reciente en lugar de borrarse. maxTracks limita la cantidad en lugar de la antigüedad: al superarlo, en la siguiente escritura se descarta la pista registrada hace más tiempo, y registrar una pista, aunque sea una posición anterior, la mueve al final de la cola. Todas las opciones están en la referencia de la API del core.
Siguientes pasos
- Referencia de la API del core - todas las props disponibles
- Guía del framework - componentes, demos e integraciónGuía del framework - componentes, demos e integraciónGuía del framework - componentes, demos e integración
- Reel Player - reproductor de vídeo al estilo de TikTok/ReelsReel Player - reproductor de vídeo al estilo de TikTok/ReelsReel Player - reproductor de vídeo al estilo de TikTok/Reels
- Lightbox - galería de imágenes y vídeosLightbox - galería de imágenes y vídeosLightbox - galería de imágenes y vídeos
- Stories Player - visor de stories al estilo de InstagramStories Player - visor de stories al estilo de InstagramStories Player - visor de stories al estilo de Instagram