Stories Core
O motor por trás de @reelkit/react-stories-player. TypeScript puro, sem dependência de framework. Use para montar players de stories em Angular, Vue ou JavaScript puro.
Instalação
npm i @reelkit/stories-coreControlador do Stories
createStoriesController(config, events?) devolve um StoriesController que cuida da navegação entre grupos e stories. Acompanha o estado de pausa, lembra o último story visto de cada grupo e dispara callbacks a cada transição.
Configuração (StoriesControllerConfig)
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
groupCount | number | obrigatório | Número total de grupos de stories |
storyCounts | number[] | obrigatório | Quantidade de stories em cada grupo |
initialGroupIndex | number | 0 | Índice do grupo inicial |
initialStoryIndex | number | resumeStoryIndex(initialGroupIndex), senão 0 | Índice do story inicial dentro do grupo. Informar um valor vence qualquer coisa lembrada; deixe de fora e o grupo de abertura retoma como todos os outros. |
defaultImageDuration | number | 5000 | Duração padrão, em ms, do avanço automático dos stories de imagem |
resumeStoryIndex | (groupIndex: number) => number | undefined | Story em que um grupo ainda não visitado abre, limitado a um story que o grupo tenha |
Eventos (StoriesControllerEvents)
| Evento | Tipo | Descrição |
|---|---|---|
onStoryChange | (groupIndex, storyIndex) => void | Disparado quando o story ativo muda |
onGroupChange | (groupIndex) => void | Disparado quando o grupo ativo muda |
onStoryViewed | (groupIndex, storyIndex) => void | Disparado quando um story fica visível |
onStoryComplete | (groupIndex, storyIndex) => void | Disparado quando o timer do story termina (antes de avançar) |
onComplete | () => void | Disparado quando o último story do último grupo acaba |
onClose | () => void | Disparado quando o overlay deve fechar |
Estado (sinais reativos)
| Sinal | Tipo | Descrição |
|---|---|---|
state.activeGroupIndex | Signal<number> | Índice do grupo ativo no momento |
state.activeStoryIndex | Signal<number> | Índice do story ativo dentro do grupo |
state.isPaused | Signal<boolean> | Se o avanço automático está pausado |
Métodos
| Método | Tipo | Descrição |
|---|---|---|
nextStory() | () => void | Avança dentro do grupo; ao chegar ao fim, passa ao grupo seguinte |
prevStory() | () => void | Volta dentro do grupo; ao chegar ao início, passa ao grupo anterior |
nextGroup() | () => void | Muda para o próximo grupo, retomando no último story visto |
prevGroup() | () => void | Muda para o grupo anterior, retomando no último story visto |
goToGroup(index) | (number) => void | Salta direto para um grupo pelo índice |
pause() | () => void | Pausa o avanço automático |
resume() | () => void | Retoma o avanço automático |
onStoryTimerComplete() | () => void | Chamado quando o timer termina; dispara onStoryComplete e então avança |
getLastStoryIndex(groupIndex) | (number) => number | Onde o grupo abre: o story em que foi deixado nesta sessão, senão o que resumeStoryIndex indicar |
reportInitialView() | () => void | Registra, uma única vez, como visto o story em que o player abriu. Chame depois da montagem, não durante a renderização. |
updateConfig({ groupCount, storyCounts }) | (config) => void | Substitui a contagem de grupos e de stories quando o feed muda com o player aberto, para que os grupos carregados depois possam ser alcançados. Não dispara nenhum evento, mantém onde cada grupo foi deixado e traz as posições de volta para dentro de um feed que encolheu. |
Exemplo
import { createStoriesController } from '@reelkit/stories-core';
import { createTimerController, reaction } from '@reelkit/core';
const groups = [
{ stories: ['s1', 's2', 's3'] },
{ stories: ['s4', 's5'] },
];
const controller = createStoriesController(
{
groupCount: groups.length,
storyCounts: groups.map((g) => g.stories.length),
defaultImageDuration: 5000,
},
{
onStoryChange(groupIndex, storyIndex) {
console.log('Story changed:', groupIndex, storyIndex);
},
onComplete() {
console.log('All stories viewed');
},
onClose() {
console.log('Overlay closed');
},
},
);
// Wire up a timer for auto-advance
const timer = createTimerController({
duration: 5000,
onComplete: () => controller.onStoryTimerComplete(),
});
// React to story changes and restart the timer
const dispose = reaction(
() => [
controller.state.activeGroupIndex,
controller.state.activeStoryIndex,
],
() => timer.start(),
);
// Start playback
timer.start();
// Navigation
controller.nextStory();
controller.pause();
controller.resume();
// Cleanup
dispose();
timer.dispose();Controlador de timer
O timer que conduz o progresso de cada story fica em @reelkit/core como createTimerController, compartilhado com a apresentação do lightbox. Importe-o de @reelkit/core ou da sua binding (@reelkit/react, @reelkit/vue, @reelkit/angular); este pacote não o exporta mais. Veja Reprodução automática na API do core para sua configuração, estado e métodos, e o exemplo do Stories Controller acima para ligá-lo a onStoryTimerComplete.
Renderizador de progresso em Canvas
createCanvasProgressRenderer(config?) desenha barras de progresso segmentadas em um canvas. Ajusta a escala para telas Retina, acompanha a largura do contêiner com observeElementSize de @reelkit/core e passa a uma janela deslizante quando os segmentos não cabem.
Configuração (CanvasProgressRendererConfig)
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
gap | number | 2 | Espaço em pixels entre os segmentos |
barHeight | number | 2 | Altura da barra em pixels |
minSegmentWidth | number | 8 | Largura mínima do segmento antes de a janela deslizante entrar em cena |
bgColor | string | 'rgba(255,255,255,0.3)' | Cor de fundo dos segmentos ainda não preenchidos |
fillColor | string | '#ffffff' | Cor de preenchimento dos segmentos concluídos e do ativo |
onResize | () => void | - | Chamado depois que o contêiner mudou de tamanho e o canvas foi medido de novo. Redimensionar um canvas o apaga, então redesenhe aqui. |
Métodos
createCanvasProgressRenderer devolve um CanvasProgressRenderer, o objeto a guardar quando você tipa um campo de componente ou uma ref que mantém o renderizador:
| Membro | Tipo | Descrição |
|---|---|---|
attach(canvas) | (HTMLCanvasElement) => void | Liga a um elemento canvas; passa a acompanhar o tamanho do elemento pai |
draw(totalStories, activeIndex, progress) | (number, number, number) => void | Desenha a barra de progresso para o estado informado |
width | number (readonly) | Largura medida no momento, em pixels de CSS |
dispose() | () => void | Para de acompanhar o tamanho do elemento pai e limpa o estado interno |
Exemplo
import { createCanvasProgressRenderer } from '@reelkit/stories-core';
const renderer = createCanvasProgressRenderer({
gap: 2,
barHeight: 2,
fillColor: '#ffffff',
bgColor: 'rgba(255, 255, 255, 0.3)',
});
// Attach to a canvas element
const canvas = document.querySelector('canvas')!;
renderer.attach(canvas);
// Draw on each animation frame
let frameId: number;
function loop() {
const totalStories = 5;
const activeIndex = 2;
const progress = timer.progress.value; // 0-1
renderer.draw(totalStories, activeIndex, progress);
frameId = requestAnimationFrame(loop);
}
frameId = requestAnimationFrame(loop);
// Cleanup
cancelAnimationFrame(frameId);
renderer.dispose();Controlador do que já foi visto
createStoriesViewedStateController(config) reúne, em uma única chamada, tudo o que um feed precisa para lembrar o que já foi visto: as contagens dos anéis, onde cada grupo é retomado e o registro do que foi visto, sobre um armazenamento que sobrevive a um recarregamento. Ele não importa nenhum framework, então React, Vue e Angular o usam do mesmo jeito: crie-o uma vez onde o feed vive e passe-o para a lista de anéis e para o player, que cuidam de conectá-lo e de acompanhá-lo. Ele é montado sobre o createViewedStateController do core com a chave de dois eixos por ids estáveis; recorra a eles diretamente quando isto não servir. A entrada nomeia o story mais distante alcançado, e não um total de visualizações, então o lugar do grupo sobrevive à reordenação do feed, enquanto um story removido do meio de um grupo encurta a contagem e acende o anel de novo.
Configuração (StoriesViewedStateControllerConfig)
| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
storageKey | string | obrigatório | Chave de armazenamento sob a qual as entradas são gravadas |
groups | () => StoriesGroup<T>[] | obrigatório | Lê os grupos atuais. É um getter, chamado toda vez, então um feed que carrega mais grupos só precisa devolvê-los. Mantenha-o atualizado: um getter preso ao primeiro array conta errado todos os grupos carregados depois. |
key | UrlKey<Id, TwoAxisPosition> | ids estáveis | Como uma posição é escrita no armazenamento. Passe a chave que o controlador de URL usa e uma entrada será lida como um link compartilhado. |
storage | StorageAdapter | localStorage | Onde as entradas ficam guardadas. Um adaptador próprio também é a forma de plugar o seu próprio repositório do que foi visto. |
ttlMs | number | não expira | Por quanto tempo a entrada de um grupo fica lembrada depois do último registro. Cada grupo expira no seu próprio relógio. |
maxTracks | number | todos mantidos | Quantos grupos manter. Acima disso, o grupo registrado há mais tempo é descartado na próxima gravação. |
storageKey, key e storage são lidos uma única vez, na criação do controlador. Para trocar de armazenamento, crie outro controlador.
StoriesViewedStateController
| Membro | Tipo | Descrição |
|---|---|---|
viewedState | Subscribable<Map<string, number>> | Stories vistos por id de autor, como signal. Vazio antes de attach(). |
controller | ViewedStateController<TwoAxisPosition> | O repositório por baixo, para o que isto não cobrir |
resumeStoryIndex(groupIndex) | (number) => number | Primeiro story ainda não visto, ou 0 quando o grupo já foi assistido até o fim |
markViewed(groupIndex, storyIndex) | (number, number) => void | Registra um story como visto. O player o chama para cada story mostrado. |
forget() | () => void | Esquece tudo o que foi visto, aqui e no armazenamento |
attach() | () => Dispose | Lê o armazenamento e acompanha as outras abas. É contado, então uma lista de anéis e um player podem se conectar e ser desmontados em qualquer ordem. Os componentes do player o chamam por conta própria. |
Exemplo
import { createStoriesViewedStateController } from '@reelkit/stories-core';
// Once, where the feed lives. The getter is read every time, so it has to
// return the groups as they are now.
const viewed = createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => groups,
});
// The player components do this themselves when given `viewed`.
const detach = viewed.attach();
viewed.viewedState.value; // Map { 'user_42' => 2 }, and a signal to follow
viewed.resumeStoryIndex(0); // 2 — the first story not yet seen
viewed.markViewed(0, 2); // furthest point wins; a rewatch never rewinds
viewed.forget(); // clears it all
detach();Funções utilitárias
Funções puras para detectar as zonas de toque e para a matemática da barra de progresso.
| Função | Tipo | Descrição |
|---|---|---|
getTapAction(tapX, containerWidth, splitRatio?) | (number, number, number?) => 'prev' | 'next' | Decide, pela posição, se um toque aciona 'prev' ou 'next'. O splitRatio padrão é 0.3. |
getSegments(totalStories, activeIndex, progress) | (number, number, number) => SegmentState[] | Calcula o estado e a porcentagem preenchida de cada segmento da barra de progresso |
getVisibleWindow(totalStories, activeIndex, progress, containerWidth, minSegmentWidth?, gap?) | (number, number, number, number, number?, number?) => VisibleWindow | Calcula a janela deslizante de segmentos visíveis quando a quantidade total passa da capacidade do contêiner |
Funções de layout
Funções puras que o player de cada binding compartilha, para React, Vue e Angular calcularem tamanho e disposição do mesmo jeito. Quem as chama é o binding; a sua aplicação só precisa delas se for montar um player próprio.
Dimensionamento
| Função | Tipo | Descrição |
|---|---|---|
getStoriesSize() | () => [number, number] | Tamanho da tela do story. No celular ocupa a tela inteira; no desktop ocupa a altura da janela menos uma margem de 16px, em 9:16, e só estreita quando a tela e as duas setas não cabem lado a lado. Sem window (renderização no servidor) devolve [0, 0]. |
parseDurationMs(value) | (string) => number | O maior tempo de uma lista de transition-duration calculada, em milissegundos. O que não for tempo conta como zero. |
O limite de celular que o getStoriesSize usa é o isMobileWidth, exportado por @reelkit/core e por todos os bindings: 768px ou menos é celular. Veja Detecção de tamanho.
Geometria do carrossel no desktop
Os deslocamentos são relativos ao grupo ativo: negativos à esquerda, positivos à direita, 0 para o próprio story ativo.
| Função | Tipo | Descrição |
|---|---|---|
getCardSize(activeSize) | ([number, number]) => [number, number] | Tamanho de um cartão lateral: 0,4 da altura do story ativo, na mesma proporção 9:16. |
getCarouselSlot(offset, activeSize) | (number, [number, number]) => CarouselSlot | Onde um cartão fica em relação ao centro da janela: { x, width, height }. O primeiro cartão de cada lado passa da seta; cada seguinte soma uma largura de cartão e um espaço. |
getCardOffsets(activeGroupIndex, groupCount) | (number, number) => number[] | Deslocamentos dos cartões desenhados ao redor do grupo ativo em repouso, até dois por lado, sem o próprio ativo. |
isCardShown(offset) | (number) => boolean | Se um cartão a essa distância é desenhado em vez de esmaecido. |
getSlotOffset(offset) | (number) => number | Posição para onde um cartão vai, limitada a uma casa além das visíveis, para o cartão entrar deslizando ao lado do último visível. |
getSlideGroupIndexes(from, to, groupCount) | (number, number, number) => number[] | Grupos que um deslize entre dois grupos desenha: os cartões em volta de cada ponta, em ordem de grupo. Os grupos por que um salto longo passa ficam de fora. |
Apresentação
| Função | Tipo | Descrição |
|---|---|---|
getRingPresentation(options) | (RingPresentationOptions) => RingPresentation | Classe, estilo inline e tamanho do avatar de um anel de story, a partir de totalStories, viewedCount e size. Com algo por ver vira o gradiente cônico girando; visto até o fim fica um viewedColor liso; grupo vazio não ganha anel. Os tamanhos levam a unidade px, então o objeto de estilo serve em qualquer framework. As cores vêm dos tokens --rk-stories-ring-active-gradient e --rk-stories-ring-viewed-color, então a página as troca pelo CSS; gradientColors e viewedColor definem a cor de um só anel. |
kStoriesRingSize, kStoriesRingListRingSize, kStoriesCardRingSize | number | Diâmetros de anel que toda binding desenha por padrão, em pixels: 68 para um anel avulso, 64 na lista de anéis, 52 num card do carrossel de desktop. Passe um deles como size para ficar igual aos players. |
getPreviewSource(story) | (StoryItem | undefined) => string | undefined | Imagem que um cartão do carrossel mostra: o poster do story, a própria imagem quando é uma imagem, e nada para um vídeo sem poster. Um cartão sem imagem cai no cartão simples desenhado para um story sem nada a mostrar. |
formatTimeAgo(date) | (string | Date) => string | Tempo relativo curto que o cabeçalho e os cartões do carrossel mostram: now, 5m, 3h, 2d, 2w. |
Os tipos CarouselSlot, RingPresentation e RingPresentationOptions são exportados junto com elas.
Tipos
Todas as definições de tipo exportadas por @reelkit/stories-core.
type MediaType = 'image' | 'video';
interface StoryItem {
id: string;
mediaType: MediaType;
src: string;
poster?: string;
duration?: number;
createdAt?: string | Date;
aspectRatio?: number;
}
interface AuthorInfo {
id: string;
name: string;
avatar: string;
verified?: boolean;
}
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}
type SegmentStatus = 'completed' | 'active' | 'upcoming';
interface SegmentState {
status: SegmentStatus;
fillPercentage: number; // 0-100
}
interface VisibleWindow {
startIndex: number;
endIndex: number;
segments: SegmentState[];
}
type TapAction = 'prev' | 'next';
interface CarouselSlot {
x: number;
width: number;
height: number;
}
interface RingPresentationOptions {
totalStories: number;
viewedCount: number;
size: number; // px
gradientColors?: string[];
viewedColor?: string;
}
interface RingPresentation {
avatarSize: number;
className: string;
style: Record<string, string>;
}