Guia do core
O pacote @reelkit/core traz a lógica do slider sem amarras de framework. Use-o para criar integrações próprias ou para entender a arquitetura por baixo dos componentes.
Visão geral da arquitetura
O core adota um padrão de controladores com funções fábrica. Nada de classes — apenas objetos simples devolvidos por closures. Zero dependências. O core coordena:
- SliderController — estado central e navegação
- GestureController — arrasto por toque ou ponteiro
- KeyboardController — setas e Escape
- WheelController — roda do mouse, com debounce
createSliderController
Cria uma instância do controlador que cuida de todo o estado e do comportamento do slider.
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 do controlador
Navegação
// 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);Atualizações de estado
// Update configuration
controller.updateConfig({
count: 20,
loop: true,
});Virtualização
O core mantém apenas 3 slides no DOM a qualquer momento (atual, anterior, próximo). O extrator de intervalo decide quais índices entram na janela 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;
};O resultado é sempre limitado a no máximo 3 índices. Se o seu extrator devolver mais, o core fica com 3 centrados no slide atual.
Sinais
O core usa um sistema de sinais enxuto para a reatividade:
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 do controlador
Acesse o estado reativo por 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 da linha do tempo
Monte uma barra de progresso arrastável para qualquer elemento <video>. O controlador expõe sinais reativos de duração, tempo atual, trechos em buffer e estado do arrasto, e liga as interações de ponteiro e teclado a qualquer elemento do DOM em uma única chamada.
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 na URL
Leve o estado aberto de um overlay para a barra de endereços: o slide visível ganha um link que dá para compartilhar, abrir direto e fechar com o botão voltar. O modelo pertence ao core; os bindings o embrulham em um hook (useOverlayUrlState no React e no Vue, createOverlayUrlState no Angular) e em um componente de overlay guiado pela URL.
Como funciona
createUrlStateController espelha um parâmetro de consulta em um sinal e escreve as mudanças de volta. Abrir empilha uma entrada no histórico; cada navegação seguinte substitui essa entrada — cem deslizes não acrescentam nenhuma, então um único passo para trás sempre fecha. O UrlAdapter é a costura conectável de leitura e escrita: o padrão aciona history.pushState, e um app com rotas passa um adaptador ligado ao roteador, para que a localização do próprio roteador nunca fique desatualizada.
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 e locator — dois papéis
Uma chave é um par combinado { codec, locator }. Escrever uma identidade na URL e descobrir onde ela está agora são preocupações distintas, por isso vivem em objetos distintos:
- codec — o fio.
encodeescreve uma identidade no texto do parâmetro;decodefaz o caminho inverso e rejeita valores malformados, de modo que o parâmetro se cura sozinho saindo da URL. - locator — a busca.
locateencontra onde a identidade decodificada está na coleção atual (ounull, se sumiu);identifyfaz o caminho de volta, da posição à identidade, na hora de escrever;locateAsync, opcional, pagina um feed em janela ou infinito quando a busca falha.
Manter os dois separados permite combinar qualquer fio com qualquer busca — um codec de id estável com um locator que pagina, por exemplo.
Chaves por índice e por id estável
Duas chaves prontas montam esse par por você; a diferença está apenas no que a URL nomeia:
urlIndexKey(() => count)endereça pela posição (?photo=3). É a mais simples, mas um favorito abre outro item assim que a lista muda de ordem.urlStableIdKey({ items })endereça peloidestável de cada item (?photo=post_42), varrendo a lista atual — o favorito continua apontando para aquele item depois de uma reordenação, ou se desfaz sem sujeira quando o item some.hashCodec: base64UrlCodecdisfarça o id em base64url (reversível, não é hash criptográfico).
Feed em janela, carregado aos poucos? As duas chaves prontas aceitam um locateAsync opcional — urlIndexKey(() => count, locateAsync) e urlStableIdKey({ items, locateAsync }). A busca síncrona responde pelo que já carregou; quando ela falha, o resto é paginado, então um link compartilhado que aponta além da janela ainda abre — sem codec nem locator escritos à mão.
Dois eixos? urlIndexTwoAxisKey carrega ?p=<externo>.<interno> para um post mais o índice da mídia interna. As opções completas estão na referência da API do core.
Itens já vistos
Lembre até onde o leitor chegou em uma galeria, entre recarregamentos e entre abas — um anel que mostra o que já foi visto, uma galeria que reabre onde parou. É o mesmo modelo do estado na URL, apontado para o armazenamento em vez da barra de endereços, e por isso os dois compartilham a mesma chave.
Como funciona
createViewedStateController guarda uma entrada com exatamente o texto que um link ?photo= carregaria, e a lê de volta pelo mesmo ciclo de decode seguido de locate. Nada confia em uma posição armazenada. Espalhe uma única chave nas duas superfícies e o favorito e a entrada guardada passam a ser a mesma string.
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 } | nullA durabilidade acompanha a chave
O armazenamento não acrescenta nenhum reparo próprio, então a chave que você espalha decide o que sobrevive: uma chave endereçada por id mantém o lugar mesmo que a coleção mude de ordem; uma endereçada por posição, não. A entrada guardada nomeia o ponto mais distante alcançado, e não um total de visualizações, então remover um item do meio encurta a contagem — a mesma cura espontânea que um link compartilhado recebe.
A leitura é só síncrona. Uma entrada cujos itens ainda não carregaram é lida como ausente e permanece intacta no armazenamento, de forma que um feed em janela nunca devore o próprio histórico.
Armazenamento e expiração
Por padrão o localStorage sustenta o armazenamento; createSessionStorageAdapter() esquece tudo ao fechar a aba, e um StorageAdapter seu leva os dados para qualquer lugar síncrono. Nada é lido antes de attach(), então a renderização no servidor e a primeira renderização no cliente concordam.
As entradas ficam guardadas até serem esquecidas explicitamente. Passe ttlMs para que expirem — por trilha, em um relógio deslizante, para que algo ainda em uso não envelheça junto com algo abandonado. Isso muda o que é gravado, e cada entrada vira um par [wire, timestamp], mas a leitura entende os dois formatos independentemente da opção, então uma entrada guardada antes de você ligar o recurso conta como recente em vez de ser apagada. maxTracks limita a quantidade em vez da idade: passando do limite, a trilha registrada há mais tempo cai na próxima escrita, e registrar uma trilha, mesmo em uma posição já vista, a manda para o fim da fila. As opções completas estão na referência da API do core.
Reprodução automática
Temporização para tudo o que avança sozinho. O Stories Player roda cada story com createTimerController; o Lightbox roda a apresentação com createSlideshowController, que se apoia nele. Os dois avançam com requestAnimationFrame, e toda pausa guarda o progresso exato.
createTimerController
Conta uma duração de 0 a 1 e avisa quando termina. start() sempre começa do 0, opcionalmente com outra duração para aquela execução; pause() e resume() guardam a posição exata.
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
A máquina de estados por trás de uma galeria que avança sozinha. Ela só roda enquanto está ligada, nada a segura e a página está visível; acompanha a visibilidade da página por conta própria, e você fornece o resto. Uma imagem conta o intervalo a partir do momento em que carregou, então uma imagem lenta nunca é pulada sem ser vista. Um vídeo avança quando termina, então precisa tocar uma vez em vez de repetir. Um vídeo que termina enquanto algo segura a apresentação avança quando ela é liberada.
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();A configuração, o estado e os métodos completos estão na referência da API do core.
Download
downloadFile(url, filename?) salva um arquivo com o nome filename, por padrão o último segmento do caminho da URL. Um link de outra origem ignora o atributo download, então ele baixa o arquivo com fetch e salva o blob. Quando o fetch falha ou o servidor responde com erro — o caso comum é um servidor de outra origem sem cabeçalhos CORS —, a URL abre em uma nova aba, e o leitor ainda recebe o arquivo. Nunca rejeita e não faz nada sem uma janela. Veja a referência da API do core.
Próximos passos
- Referência da API do core - todas as props disponíveis
- Guia do framework - componentes, demonstrações e integraçãoGuia do framework - componentes, demonstrações e integraçãoGuia do framework - componentes, demonstrações e integração
- Reel Player - player de vídeo no estilo TikTok/ReelsReel Player - player de vídeo no estilo TikTok/ReelsReel Player - player de vídeo no estilo TikTok/Reels
- Lightbox - galeria de imagens e vídeosLightbox - galeria de imagens e vídeosLightbox - galeria de imagens e vídeos
- Stories Player - visualizador de stories no estilo InstagramStories Player - visualizador de stories no estilo InstagramStories Player - visualizador de stories no estilo Instagram