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.
Reproducción automática
Temporización para todo lo que avanza solo. Stories Player ejecuta cada story con createTimerController; Lightbox ejecuta su presentación con createSlideshowController, que se apoya en él. Los dos avanzan con requestAnimationFrame, y cada pausa conserva el progreso exacto.
createTimerController
Cuenta una duración de 0 a 1 y avisa al terminar. start() siempre empieza desde 0, opcionalmente con otra duración para esa ejecución; pause() y resume() conservan la posición exacta.
import { createTimerController, reaction } from '@reelkit/core';
const timer = createTimerController({
duration: 5000,
onComplete: () => console.log('Timer finished!'),
});
// Observe progress (0 to 1)
const dispose = reaction(
() => [timer.progress],
() => {
console.log('Progress:', timer.progress.value);
},
);
// Start with default duration
timer.start();
// Or override duration for a specific story
timer.start(8000);
// Pause/resume preserves exact position
timer.pause();
timer.resume();
// Reset to 0
timer.reset();
// Cleanup
dispose();
timer.dispose();createSlideshowController
La máquina de estados detrás de una galería que avanza sola. Solo corre mientras está activada, nada la retiene y la página está visible; vigila la visibilidad de la página por sí misma, y tú le das el resto. Una imagen cuenta su intervalo desde que termina de cargar, así que una lenta nunca se salta sin verse. Un vídeo avanza cuando termina, así que debe reproducirse una vez en lugar de en bucle. Un vídeo que termina mientras algo retiene la presentación avanza al liberarse.
import { createSlideshowController, createSliderController } from '@reelkit/core';
const items = [{ type: 'image' }, { type: 'video' }, { type: 'image' }];
const slider = createSliderController(
{ count: items.length, direction: 'horizontal' },
{ onAfterChange: (index) => onSlideChange(index) },
);
const slideshow = createSlideshowController({
interval: 5000,
onAdvance: () => {
if (slider.state.index.value >= items.length - 1) slideshow.pause();
else slider.next();
},
});
// Every slide change starts over and waits for the new slide.
function onSlideChange(index: number) {
slideshow.setSlide(items[index].type === 'video' ? 'video' : 'image');
}
// From the slide: an image counts its interval once it loaded or failed,
// a video advances when it ends (let it play once rather than loop).
const onImageSettled = () => slideshow.ready();
const onVideoEnded = () => slideshow.ended();
// Holds stack: the slideshow runs again once every reason is released.
const onControlsEnter = () => slideshow.hold('hover');
const onControlsLeave = () => slideshow.release('hover');
onSlideChange(0);
slideshow.play();
// Later
slideshow.dispose();La configuración, el estado y los métodos completos están en la referencia de la API del core.
Descarga
downloadFile(url, filename?) guarda un archivo con el nombre filename, por defecto el último segmento de la ruta de la URL. Un enlace de otro origen ignora el atributo download, así que descarga el archivo con fetch y guarda el blob. Cuando el fetch falla o el servidor responde con un error —lo habitual es un servidor de otro origen sin cabeceras CORS—, la URL se abre en una pestaña nueva, así que el lector igual obtiene el archivo. Nunca rechaza y no hace nada sin una ventana. Consulta 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