Stories Player para Vue
Overlay de stories no estilo Instagram para Vue 3, com @reelkit/vue-stories-player. Ele usa o mesmo motor @reelkit/stories-core do player React, então o comportamento, as classes CSS e os tokens de tema são os mesmos.
Features
Instalação
npm install @reelkit/vue-stories-player @reelkit/vue lucide-vue-nextImporte a folha de estilos uma vez no ponto de entrada do app (ou em qualquer componente):
import '@reelkit/vue-stories-player/styles.css';Ícones
O cabeçalho padrão e as setas de navegação usam ícones do lucide-vue-next. Se você prefere outra biblioteca de ícones, substitua-os pelos slots com escopo #header e #navigation.
Vue 3.3 ou mais recente
Os overlays são componentes genéricos, que o Vue suporta a partir da 3.3. Num Vue mais antigo o runtime descarta as props e os eventos deles, por isso o pacote exige vue 3.3 ou mais recente como peer dependency.
Início rápido
StoriesOverlay renderiza o player em tela cheia. Combine-o com StoriesRingList para os pontos de entrada no estilo Instagram, passe um array de objetos StoriesGroup e use v-model:is-open.
<script setup lang="ts">
import { ref } from 'vue';
import {
StoriesOverlay,
StoriesRingList,
type StoriesGroup,
} from '@reelkit/vue-stories-player';
import '@reelkit/vue-stories-player/styles.css';
const groups: StoriesGroup[] = [
{
author: { id: 'alice', name: 'Alice', avatar: '/alice.jpg', verified: true },
stories: [
{ id: 's1', mediaType: 'image', src: '/story-1.jpg' },
{ id: 's2', mediaType: 'video', src: '/story-2.mp4', poster: '/poster-2.jpg' },
],
},
{
author: { id: 'bob', name: 'Bob', avatar: '/bob.jpg' },
stories: [{ id: 's3', mediaType: 'image', src: '/story-3.jpg' }],
},
];
const isOpen = ref(false);
const groupIndex = ref(0);
const openGroup = (index: number) => {
groupIndex.value = index;
isOpen.value = true;
};
</script>
<template>
<StoriesRingList :groups="groups" @select="openGroup" />
<!-- v-model:is-open keeps the ref in sync however the viewer closes it:
the ✕ button, a swipe down, Escape, or the last story finishing. -->
<StoriesOverlay
v-model:is-open="isOpen"
:groups="groups"
:initial-group-index="groupIndex"
/>
</template>Estado na URL
Ver demo ao vivo →StoriesUrlOverlay é um componente separado cujo estado de abertura vive na barra de endereços. Os dois eixos viajam em um único parâmetro — ?story=<group>.<story> — então o story em reprodução tem um link que dá para compartilhar, favoritar e fechar com o botão voltar. Crie um controlador com useOverlayUrlState e urlIndexTwoAxisKey e entregue-o como controller.
Chaves prontas
Stories têm dois eixos, então espalhe uma chave de dois eixos no controlador: urlIndexTwoAxisKey (autor e story por posição) ou urlStableIdTwoAxisKey (o autor por um id estável) — as duas são reexportadas de @reelkit/vue-stories-player. Veja o guia de estado na URL e a Core API.
<script setup lang="ts">
import {
StoriesUrlOverlay,
useOverlayUrlState,
urlIndexTwoAxisKey,
type StoriesGroup,
} from '@reelkit/vue-stories-player';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-stories-player/styles.css';
const props = defineProps<{ groups: StoriesGroup[] }>();
// The outer axis is the group, the inner one the story inside it.
const stories = useOverlayUrlState({
param: 'story',
adapter: useVueRouterUrlAdapter(),
...urlIndexTwoAxisKey({
outerCount: () => props.groups.length,
innerCounts: () => props.groups.map((group) => group.stories.length),
}),
});
</script>
<template>
<!-- Opening is a link — the overlay reads the URL and opens itself. -->
<RouterLink
v-for="(group, index) in props.groups"
:key="group.author.id"
:to="`?story=${index}.0`"
>
{{ group.author.name }}
</RouterLink>
<StoriesUrlOverlay :controller="stories" :groups="props.groups" />
</template>- Abrir empilha uma entrada no histórico. Andar pelos stories e trocar de autor apenas a substituem, então N navegações não acrescentam entradas e um passo para trás sempre fecha o player. Voltar fecha; não retrocede um story.
- A navegação interna também vai na URL. Avançar dentro dos stories de um autor atualiza
?story=2.n, então um link direto cai no story exato. - Voltar só fecha quando o player foi aberto de dentro do app — o link empilhou uma entrada. Um link compartilhado aberto numa aba nova não tem histórico atrás dele, então o voltar do navegador sai do site; o botão ✕ ou Escape remove o parâmetro no lugar e permanece.
- Um parâmetro que não nomeia autor nem story — um favorito velho, um valor editado à mão, um story além do fim do grupo — é removido da URL em vez de abrir o vizinho.
- Um app com rotas deve passar um adaptador ligado ao roteador, para que ele continue sendo a única fonte de verdade da navegação.
useVueRouterUrlAdapter, de@reelkit/vue/vue-router-url-adapter, é o adaptador pronto para o Vue Router; escrever no histórico por trás do roteador deixa a location dele desatualizada e derruba o parâmetro na navegação seguinte.
Links estáveis. O autor é posicional por padrão, então um ?story=2.0 favoritado abre outra pessoa assim que o feed é reordenado. Enderece o autor por um id estável — outerCodec escreve o id na URL e outerLocator encontra onde ele está. Passe innerCodec com innerLocate e innerIdentify para fazer o mesmo com o story dentro do grupo; sem eles, a metade do story continua sendo um índice simples dentro do grupo resolvido. Um id de story não pode conter ., o delimitador entre as duas metades.
// `?story=alice.0` — the group by the author's stable id, the story by its
// position inside whichever group that id resolves to.
const stories = useOverlayUrlState({
param: 'story',
adapter: useVueRouterUrlAdapter(),
...urlIndexTwoAxisKey({
outerCount: () => groups.value.length,
innerCounts: () => groups.value.map((group) => group.stories.length),
outerCodec: { decode: (raw) => raw, encode: (id) => id },
outerLocator: {
locate: (id) =>
groups.value.findIndex((group) => group.author.id === id),
identify: (index) => groups.value[index].author.id,
},
}),
});Feeds infinitos. Paginar é assunto do outerLocator, independente do codec. locate é síncrono, então só responde por grupos já carregados — um link compartilhado para o grupo 400 de um feed que carregou 20 não encontra nada. locateAsync é o plano B, chamado apenas quando locate erra; o story é então limitado ao grupo em que tudo parou.
O mesmo locateAsync, eixo externo
É o mesmo paginador locateAsync que as chaves de um eixo aceitam — numa chave de dois eixos ele viaja no outerLocator que você passa, então o eixo dos autores pagina enquanto o story continua um índice local dentro do grupo resolvido.
// A feed that pages. `locate` answers for the groups already loaded;
// `locateAsync` runs only when it misses, so a shared link to a group past
// the window fetches up to it and then opens.
outerLocator: {
locate: (index) => (index < loaded.value.length ? index : null),
identify: (index) => index,
locateAsync: async (index) => {
if (index >= totalGroups) return null;
loaded.value = await fetchGroupsUpTo(index);
return index;
},
},- Enquanto
locateAsyncestá pendente, o player fica fechado e o parâmetro intacto, então o link direto sobrevive à busca. Umnullou uma rejeição removem o parâmetro. - Uma resposta que chega depois de a URL ter mudado, depois do fechamento ou depois da desmontagem é descartada — uma busca lenta não abre um story que ninguém pediu mais.
- As opções completas de
useOverlayUrlStateestão na referência da API Vue.
Lembrando o que já foi visto
Os anéis mostram o gradiente até um autor ser visto até o fim, e um grupo reabre no primeiro story que a pessoa ainda não viu. Os dois vêm de um único createStoriesViewedStateController, criado onde o feed vive e entregue à lista de anéis e ao player como viewed: eles leem o armazenamento, acompanham-no e registram o que foi mostrado por conta própria. Cada entrada guarda autor e story por id, então sobrevive à reordenação do feed, e o mesmo controlador funciona em React e Angular.
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import {
StoriesOverlay,
StoriesRingList,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/vue-stories-player';
const props = defineProps<{ groups: StoriesGroup[] }>();
const isOpen = ref(false);
const groupIndex = shallowRef(0);
// One controller for the rings and the player. It reads the groups through
// the getter every time it needs them, so a feed that pages in more groups
// is counted too.
const viewed = createStoriesViewedStateController({
storageKey: 'stories-seen',
groups: () => props.groups,
});
const openGroup = (index: number) => {
groupIndex.value = index;
isOpen.value = true;
};
</script>
<template>
<!-- The rings follow the controller by themselves. -->
<StoriesRingList
:groups="props.groups"
:viewed="viewed"
@select="openGroup"
/>
<!-- The player resumes, records, and draws its card rings from it. -->
<StoriesOverlay
v-model:is-open="isOpen"
:groups="props.groups"
:initial-group-index="groupIndex"
:viewed="viewed"
/>
</template>- O story de abertura conta. O story que já está na tela é marcado como visto assim que o player monta, então abrir um grupo de um story só e fechar já o marca como assistido.
- Seguir em frente também retoma.
resumeStoryIndexé consultado para cada grupo alcançado pela primeira vez nesta sessão, inclusive aquele em que o player abre, a menos queinitialStoryIndexnomeie um story explicitamente; um grupo já percorrido reabre onde foi deixado. - O link ainda ganha. Com
StoriesUrlOverlay, o parâmetro decide onde o player abre, independentemente do que foi armazenado. Em todo o resto, quem decide é o callback de retomada. - A contagem é uma posição, não um total. A entrada guarda o story mais distante alcançado, então acrescentar um story a um grupo já visto acende o anel de novo, e remover um do meio encurta a contagem.
- O armazenamento é trocável. Passe
storage: createSessionStorageAdapter()para esquecer ao fechar a aba, ou o seu próprioStorageAdapter. Duas abas abertas ficam em sincronia pelo evento de storage do navegador. - Um player montado só na hora de abrir lê tarde demais. O overlay lê o armazenamento depois de montar, ainda fechado. Com
v-if="open"esse momento não existe, então chameuseAttachViewedState(() => viewed)no componente dono do controlador; ele se conecta depois de montar e solta ao desmontar. As opções, e as peças de nível mais baixo por trás disso, estão na página Stories Core.
Carrossel no desktop
Use desktop-layout="carousel" para dispor o player como o visualizador web do Instagram: o story ativo fica no centro no tamanho de sempre e até dois grupos vizinhos ficam de cada lado, como cartões menores e escurecidos. Clicar num cartão abre aquele autor, e os cartões deslizam enquanto o grupo muda. Passe o mesmo controlador viewed que a lista de anéis recebe e o cartão de um grupo já visto ganha o anel apagado. Os cartões acompanham o controlador sozinhos, então marcar um story como visto repinta só eles, sem re-renderizar o player.
<template>
<!-- Neighbouring groups sit beside the active story as dimmed cards on a
desktop screen. Phones keep the plain player whatever this says. -->
<StoriesOverlay
v-model:is-open="isOpen"
:groups="groups"
:viewed="viewed"
desktop-layout="carousel"
/>
</template>- No celular fica o player simples. Até 768px de largura os cartões não são renderizados e os deslizes usam
group-transition, como no layout'single'padrão. Redimensionar através dessa largura troca o layout na hora. - Cada cartão mostra por onde o grupo abre: o story em que foi deixado nesta sessão, senão o que
resumeStoryIndexdevolve. Ele exibe o poster do vídeo ou a imagem. Um story sem nenhum dos dois, como um story de texto sobre um gradiente, é desenhado pelo seu slot#slideno tamanho do player e reduzido; um vídeo sem poster mostra o fundo do cartão. Essa prévia recebeisActive: falsee é só uma imagem: nada nela recebe foco ou clique. Deixe o que um slide faz ao montar, como reprodução ou analytics, atrás deisActive. - O timer espera o deslize. O story não avança, e
story-viewednão dispara para ele, enquanto os cartões não terminam de se mover. - Toda forma de trocar de grupo desliza: clique num cartão,
StoriesApi.goToGroup,nextGroupeprevGroup, e as setas do teclado além do fim de um grupo. Um deslize de toque move o próprio player, então não ganha um deslize por cima.
Barra de progresso e cabeçalho em cada grupo
Por padrão, uma barra de progresso e um cabeçalho ficam acima do player e passam para o novo grupo depois que ele muda. Defina chrome-placement="group" para dar a cada grupo os seus, dentro do próprio slide, e os dois giram junto com o grupo, como no Instagram.
<template>
<!-- Every group carries its own progress bar and header, so both turn with
the group instead of switching above the player once it has changed. -->
<StoriesOverlay
v-model:is-open="isOpen"
:groups="groups"
chrome-placement="group"
/>
</template>- Um grupo vizinho mostra onde parou. Enquanto o player gira até ele, a barra e o cabeçalho mostram o story em que ele vai abrir, sem nada reproduzido ainda.
- O grupo que sai fica como estava. A barra dele mantém o progresso até o giro terminar, então um grupo visto até o fim continua cheio em vez de se esvaziar no meio do giro.
- Os slots são renderizados para cada grupo na tela. Os escopos de
#progressBare#headertrazemgroupIndexeisActive; para um grupo vizinhoisActiveéfalsee os sinais de progresso ficam parados. - Um slot
#headerfica na área de swipe. Um toque nele troca de story, a menos que caia em umbutton, um link ou um elemento comrole="button", então faça de cada controle um desses. - O carrossel no desktop não muda durante o deslize. O player fica oculto enquanto os cartões deslizam, então a escolha aparece quando o grupo já está aberto.
Referência da API
Props de StoriesOverlay
StoriesOverlayProps<T>
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
aria-label | string | 'Stories player' | Nome acessível da região de diálogo; anunciado por leitores de tela quando o overlay abre |
chrome-placement | 'overlay' | 'group' | 'overlay' | Onde ficam a barra de progresso e o cabeçalho. 'overlay' desenha uma cópia acima do player; 'group' desenha uma em cada slide de grupo, e os dois giram junto com o grupo. O tipo é exportado como ChromePlacement. |
default-image-duration | number | 5000 | Duração do avanço automático de um story em imagem, em milissegundos |
desktop-layout | 'single' | 'carousel' | 'single' | Layout no desktop. 'carousel' mostra grupos vizinhos como cartões ao lado do story ativo e desliza entre grupos. No celular o story aparece sempre sozinho. O tipo é exportado como DesktopLayout. |
enable-keyboard | boolean | true | Ativa a navegação por teclado (setas esquerda/direita, Escape) |
groups | StoriesGroup<T>[] | required | Grupos de stories a exibir. Grupos ou stories acrescentados com o player aberto são detectados, tanto ao trocar o array quanto ao acrescentar nele. |
group-transition | TransitionTransformFn | cubeTransition | Transição do slider externo (de grupos). Ignorada enquanto o carrossel do desktop está visível, que desliza em vez disso. |
hide-ui-on-pause | boolean | true | Esconde a barra de progresso e o cabeçalho enquanto a pausa por toque longo dura |
initial-group-index | number | 0 | Índice do grupo visível no início, começando em zero |
initial-story-index | number | retomada, senão 0 | Índice do story visível no início, começando em zero. Nomear um vence qualquer coisa lembrada; deixe de fora e o grupo de abertura retoma como todos os outros. |
inner-transition-duration | number | 200 | Duração do fade entre stories, em milissegundos |
is-open | boolean | required | Controla a visibilidade e trava a rolagem da página enquanto está aberto. Use com v-model:is-open. |
min-segment-width | number | 8 | Largura mínima de um segmento de progresso, em pixels, antes de a janela deslizante entrar |
resume-story-index | (groupIndex: number) => number | — | Em que story um grupo abre na primeira vez que é alcançado. Consultado apenas para um grupo ainda não visitado nesta abertura, e vence viewed. |
tap-zone-split | number | 0.3 | Divisão da zona de toque (0–1). A parte esquerda volta, o resto avança. |
viewed | StoriesViewedStateController | — | O que a pessoa já viu, vindo de createStoriesViewedStateController(). Os grupos retomam no primeiro story não visto, todo story exibido é registrado e os cartões do carrossel apagam o anel de um grupo visto até o fim. Passe o mesmo controlador para StoriesRingList. |
Props de StoriesUrlOverlay
StoriesUrlOverlayProps<T>
Aceita todas as props acima, menos o trio do estado de abertura — is-open, initial-group-index, initial-story-index —, que vem do controlador.
| Prop | Tipo | Padrão | Descrição |
|---|---|---|---|
controller | UrlStateController<TwoAxisPosition> | required | Controlador de useOverlayUrlState espalhado com urlIndexTwoAxisKey. Sua position — um objeto { outer, inner } — decide se o player está aberto e onde abre; o overlay escreve de volta a cada navegação e no fechamento. |
Eventos
| Evento | Payload | Descrição |
|---|---|---|
| @api-ready | StoriesApi | Emitido assim que o player monta, com a API imperativa |
| @close | void | Emitido quando o player fecha. No StoriesUrlOverlay o parâmetro já foi limpo nesse momento. |
| @double-tap | groupIndex: number, storyIndex: number | Emitido no toque duplo, junto com a animação de coração |
| @group-change | groupIndex: number | Emitido depois que o grupo ativo muda |
| @pause | void | Emitido no toque longo ou no botão de pausa do cabeçalho |
| @resume | void | Emitido ao soltar ou no botão de play do cabeçalho |
| @story-change | groupIndex: number, storyIndex: number | Emitido depois que o story ativo muda |
| @story-complete | groupIndex: number, storyIndex: number | Emitido quando o timer de um story termina |
| @story-viewed | groupIndex: number, storyIndex: number | Emitido quando um story fica visível, inclusive o de abertura |
| @update:is-open | boolean | Emitido no fechamento; habilita `v-model:is-open` |
v-model:is-open
Use v-model:is-open para controlar o overlay com um único binding. O par :is-open + @close continua funcionando quando você precisa do evento explícito.
<script setup lang="ts">
import { ref } from 'vue';
import { StoriesOverlay } from '@reelkit/vue-stories-player';
const isOpen = ref(false);
</script>
<template>
<button type="button" @click="isOpen = true">Open stories</button>
<!-- One binding, in both directions. -->
<StoriesOverlay v-model:is-open="isOpen" :groups="groups" />
<!-- The explicit pair still works when you need the event itself. -->
<StoriesOverlay
:is-open="isOpen"
:groups="groups"
@close="isOpen = false"
/>
</template>Slots com escopo
Oito slots com escopo substituem qualquer parte do player. Cada um recebe um objeto de escopo tipado. Um slot que você não passa mantém o padrão — e um que não renderiza nada também, então um v-if que fica falso volta ao padrão em vez de deixar um buraco.
| Slot | Escopo | Descrição |
|---|---|---|
| #error | { story, storyIndex, groupIndex } | Substitui o painel interno "Content unavailable", desenhado sobre o story ativo |
| #footer | { author, story, storyIndex } | Acrescenta um rodapé sob o story do grupo ativo |
| #groupPreview | { group, groupIndex, story, offset, viewedCount, onOpen } | Preenche um cartão do carrossel; o player continua posicionando, escalando e deslizando o cartão |
| #header | { author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose } | Substitui o cabeçalho padrão (autor, pausa, som, fechar). Com chrome-placement="group" ele fica na área de swipe; veja Barra de progresso e cabeçalho em cada grupo. |
| #loading | { story, storyIndex, groupIndex } | Substitui o indicador padrão do cabeçalho enquanto o story ativo carrega |
| #navigation | { onPrevStory, onNextStory, onPrevGroup, onNextGroup } | Substitui as duas setas do desktop |
| #progressBar | { totalStories, activeIndex, progress, group, groupIndex, isActive } | Substitui a barra de progresso em canvas. activeIndex e progress são sinais do core — converta-os com toVueRef. |
| #slide | { story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded } | Substitui os slides padrão de imagem e vídeo |
<template>
<StoriesOverlay v-model:is-open="isOpen" :groups="groups">
<!-- Every slot is optional; one you leave out keeps the default, and so
does one that renders nothing. -->
<template #header="{ author, isPaused, onTogglePause, onClose }">
<header class="my-header">
<img :src="author.avatar" :alt="author.name" />
<span>{{ author.name }}</span>
<button type="button" @click="onTogglePause">
{{ isPaused ? 'Play' : 'Pause' }}
</button>
<button type="button" @click="onClose">Close</button>
</header>
</template>
<template #footer="{ author, story }">
<form class="my-reply" @submit.prevent="reply(author, story)">
<input v-model="draft" :placeholder="`Reply to ${author.name}…`" />
</form>
</template>
</StoriesOverlay>
</template>Slides personalizados
Componha ImageStorySlide e VideoStorySlide dentro de #slide para manter reprodução automática, captura de poster e sincronia de som enquanto desenha o seu layout. Chame os callbacks de ciclo de vida do escopo, ou o timer nunca começa:
| Callback | Quando chamar |
|---|---|
onReady | A imagem carregou ou o vídeo começou a tocar. Limpa o estado de carregamento e inicia o timer. |
onWaiting | O vídeo está em buffer no meio da reprodução. Mostra o indicador e pausa o timer. |
onError | O conteúdo falhou ao carregar. Mostra o estado de erro, e a URL fica marcada como quebrada, então reabrir pula a nova tentativa. |
onDurationReady | Informe a duração real da mídia em milissegundos, para o timer durar o mesmo que o vídeo. |
onEnded | A mídia terminou. Avança para o próximo story. |
<template>
<StoriesOverlay v-model:is-open="isOpen" :groups="groups">
<!-- Compose ImageStorySlide and VideoStorySlide to keep autoplay, poster
capture and sound sync while drawing your own layout around them.
Pass the callbacks through, or the timer never starts. -->
<template #slide="scope">
<div class="my-slide">
<VideoStorySlide
v-if="scope.story.mediaType === 'video'"
:src="scope.story.src"
:poster="scope.story.poster"
:group-index="scope.groupIndex"
:story-index="scope.index"
:active-group-index="scope.activeGroupIndex"
:active-story-index="scope.activeStoryIndex"
:on-duration-ready="scope.onDurationReady"
:on-playing="scope.onReady"
:on-waiting="scope.onWaiting"
:on-ended="scope.onEnded"
:on-error="scope.onError"
/>
<ImageStorySlide
v-else
:src="scope.story.src"
:on-load="scope.onReady"
:on-error="scope.onError"
/>
<!-- A carousel card draws this same slot with isActive: false, and
nothing in it can be clicked. Keep playback or analytics behind
the flag. -->
<span v-if="scope.isActive" class="my-badge">{{ scope.story.id }}</span>
</div>
</template>
</StoriesOverlay>
</template>Cache do pré-carregador
Os componentes internos ImageStorySlide e VideoStorySlide pré-carregam o próximo story em segundo plano. Quando a pessoa chega a um story já pré-carregado, o conteúdo aparece na hora, sem indicador de carregamento. Uma URL que falhou uma vez também fica guardada, então reabrir um story quebrado mostra o erro de imediato em vez de tentar de novo.
Navegação personalizada
<template>
<StoriesOverlay v-model:is-open="isOpen" :groups="groups">
<!-- Replaces both desktop arrows at once. The first node sits left of
the story, the rest to its right. -->
<template #navigation="{ onPrevStory, onNextStory, onPrevGroup, onNextGroup }">
<div class="my-nav">
<button type="button" @click="onPrevGroup">Previous user</button>
<button type="button" @click="onPrevStory">Back</button>
</div>
<div class="my-nav">
<button type="button" @click="onNextGroup">Next user</button>
<button type="button" @click="onNextStory">Forward</button>
</div>
</template>
</StoriesOverlay>
</template>Barra de progresso personalizada
O escopo carrega sinais do core em vez de números simples, então o timer move a barra sem re-renderizar o player. Converta-os com toVueRef, de @reelkit/vue:
<script setup lang="ts">
import { toVueRef, type Signal } from '@reelkit/vue';
const props = defineProps<{
totalStories: number;
activeIndex: Signal<number>;
progress: Signal<number>;
}>();
// Core signals do not re-render a Vue component on their own: bridge them.
const active = toVueRef(props.activeIndex);
const progress = toVueRef(props.progress);
</script>
<template>
<div class="my-progress">
<span
v-for="index in totalStories"
:key="index"
class="my-segment"
:style="{
'--fill':
index - 1 < active
? '100%'
: index - 1 === active
? `${progress * 100}%`
: '0%',
}"
/>
</div>
</template>Carregamento e erro personalizados
<template>
<StoriesOverlay v-model:is-open="isOpen" :groups="groups">
<!-- Both are drawn over the active story only. Without the loading slot
the default is the header spinner; without the error slot it is the
built-in "Content unavailable" panel. -->
<template #loading="{ story }">
<p class="my-loading">Loading {{ story.mediaType }}…</p>
</template>
<template #error="{ story, groupIndex }">
<div class="my-error">
<p>That {{ story.mediaType }} would not load.</p>
<button type="button" @click="report(groupIndex, story)">Report</button>
</div>
</template>
</StoriesOverlay>
</template>Cartões de carrossel personalizados
<template>
<StoriesOverlay
v-model:is-open="isOpen"
:groups="groups"
:viewed="viewed"
desktop-layout="carousel"
>
<!-- The player still positions, scales and slides the card; this fills
it. `offset` is negative on the left, positive on the right, and 0
for the card passing through the center during a slide. -->
<template #groupPreview="{ group, story, viewedCount, offset, onOpen }">
<button type="button" class="my-card" @click="onOpen">
<img v-if="story?.poster" :src="story.poster" alt="" />
<span>{{ group.author.name }}</span>
<small>{{ viewedCount }} / {{ group.stories.length }} seen</small>
<em v-if="offset === 0">opening…</em>
</button>
</template>
</StoriesOverlay>
</template>Transições
group-transition controla o efeito 3D ao trocar de autor. Importe as funções de transição de @reelkit/vue:
<script setup lang="ts">
import { StoriesOverlay } from '@reelkit/vue-stories-player';
import {
cubeTransition,
flipTransition,
fadeTransition,
zoomTransition,
slideTransition,
} from '@reelkit/vue';
</script>
<template>
<!-- cubeTransition is the default; flip, fade, zoom and slide ship too. -->
<StoriesOverlay
v-model:is-open="isOpen"
:groups="groups"
:group-transition="flipTransition"
/>
</template>StoriesApi
Ponha um template ref no overlay para controle imperativo, ou pegue a API pelo evento @api-ready. Chamadas feitas com o player fechado não fazem nada.
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { StoriesOverlay, type StoriesApi } from '@reelkit/vue-stories-player';
const isOpen = ref(false);
const player = useTemplateRef<StoriesApi>('player');
// Calls while the player is closed are safely ignored.
const skipUser = () => player.value?.nextGroup();
</script>
<template>
<button type="button" @click="skipUser">Skip this user</button>
<StoriesOverlay
ref="player"
v-model:is-open="isOpen"
:groups="groups"
@api-ready="(api) => api.pause()"
/>
</template>Métodos
| Método | Tipo | Descrição |
|---|---|---|
nextStory() | () => void | Avança para o próximo story do grupo atual |
prevStory() | () => void | Volta ao story anterior do grupo atual |
nextGroup() | () => void | Troca para o próximo grupo de autor |
prevGroup() | () => void | Troca para o grupo de autor anterior |
goToGroup(index) | (index: number) => void | Salta para um grupo específico pelo índice |
pause() | () => void | Pausa o avanço automático e o timer de progresso |
resume() | () => void | Retoma o avanço automático e o timer de progresso |
Toque duplo e curtidas
Uma animação de coração toca no toque duplo, dando retorno visual imediato. O evento @double-tap dispara com os índices de grupo e story, para você guardar a curtida no seu próprio estado. O player não mantém estado de curtida.
<script setup lang="ts">
import { StoriesOverlay, type StoriesGroup } from '@reelkit/vue-stories-player';
const props = defineProps<{ groups: StoriesGroup[] }>();
// The heart animation is built in; the like itself is yours to keep.
const like = (groupIndex: number, storyIndex: number) => {
const story = props.groups[groupIndex].stories[storyIndex];
void fetch(`/api/stories/${story.id}/like`, { method: 'POST' });
};
</script>
<template>
<StoriesOverlay
v-model:is-open="isOpen"
:groups="props.groups"
@double-tap="like"
/>
</template>Personalizando a animação de coração
Ajuste a velocidade pelo token --rk-stories-heart-duration (veja Tema). Para cor, tamanho ou para esconder o coração por completo, use a classe .rk-stories-heart. O componente HeartAnimation também é exportado para uso avulso.
:root {
/* Pop-in and fade-out duration. */
--rk-stories-heart-duration: 500ms;
}
/* Colour and size are plain CSS on the element itself. */
.rk-stories-heart svg {
width: 72px;
height: 72px;
fill: #ff4d6d;
}
/* Or hide it and animate your own from the double-tap event. */
.rk-stories-heart {
display: none;
}A animação de coração interna ainda não pode ser substituída por um slot. Reestilize-a com CSS, ou esconda-a com display: none e rode a sua própria animação a partir do evento @double-tap. Se você precisa de um slot heart, conte para a gente pelo GitHub Issues.
Subcomponentes
Blocos exportados, para compor dentro dos slots ou usar sozinhos: StoriesRing, StoriesRingList, StoryHeader, CanvasProgressBar, ImageStorySlide, VideoStorySlide e HeartAnimation.
| Componente | O que é |
|---|---|
CanvasProgressBar | A barra de progresso segmentada em canvas, desenhada direto dos sinais do timer. Cuida da janela deslizante em grupos com muitos stories. Com :live="false" só desenha quando os sinais mudam e depois de um redimensionamento, sem loop de animação. |
HeartAnimation | O coração do toque duplo. Emite complete quando a animação termina, para o pai removê-lo. |
ImageStorySlide | Slide de imagem em tela cheia com object-fit: cover, relatando carga e erro por callbacks. |
StoriesRing | Avatar circular com o anel em gradiente. Dois estados: com algo por ver ele gira, visto até o fim fica liso. Emite click. |
StoriesRingList | Fileira horizontal de anéis com o nome do autor, um por grupo. Emite select com o índice do grupo. |
StoryHeader | O cabeçalho padrão: avatar, nome, selo de verificado, tempo relativo, pausa, som, fechar e o indicador de carregamento. |
VideoStorySlide | Slide de vídeo apoiado no único elemento <video> compartilhado, para continuidade de som no iOS. Cuida da reprodução automática, dos quadros de poster e do relato de duração. |
<script setup lang="ts">
import {
CanvasProgressBar,
HeartAnimation,
ImageStorySlide,
StoriesRing,
StoriesRingList,
StoryHeader,
VideoStorySlide,
} from '@reelkit/vue-stories-player';
</script>
<template>
<!-- One ring, and the horizontal row of them. -->
<StoriesRing
:author="group.author"
:total-stories="group.stories.length"
:viewed-count="2"
:size="72"
@click="open(group)"
/>
<StoriesRingList :groups="groups" :viewed="viewed" @select="open" />
<!-- Inside a #header slot: the default header, with your own additions. -->
<StoryHeader
:author="group.author"
:created-at="story.createdAt"
:is-paused="isPaused"
:is-video="story.mediaType === 'video'"
:is-muted="isMuted"
:on-toggle-pause="onTogglePause"
:on-toggle-sound="onToggleSound"
:on-close="onClose"
/>
<!-- Inside a #progressBar slot: the canvas bar, driven by the signals. -->
<CanvasProgressBar
:total-stories="group.stories.length"
:active-index="activeIndex"
:progress="progress"
:min-segment-width="12"
/>
<!-- Inside a #slide slot; see the slide slot example above. -->
<ImageStorySlide :src="story.src" :on-load="onReady" :on-error="onError" />
<VideoStorySlide
:src="story.src"
:group-index="groupIndex"
:story-index="index"
:active-group-index="activeGroupIndex"
:active-story-index="activeStoryIndex"
/>
<!-- Standalone, outside the player. -->
<HeartAnimation @complete="remove(heart.id)" />
</template>Reexportações
Tudo de que quem usa precisa vem deste pacote, então um import direto de @reelkit/core nunca é necessário.
| De | Reexportado |
|---|---|
@reelkit/vue | Observe, SoundProvider, useSoundState, useOverlayUrlState, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec e os tipos UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, OverlayUrlStateOptions, ViewedStateController, ViewedStateOptions, StorageAdapter |
@reelkit/stories-core | createStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig e os tipos de conteúdo StoryItem, AuthorInfo, StoriesGroup, MediaType |
Observe é a fronteira de renderização que o player usa por dentro: ela re-renderiza apenas o seu slot quando um sinal do core muda, e é assim que a troca de story repinta o cabeçalho e a barra de progresso sem redesenhar os sliders. Use-a num slot seu quando um componente inteiro seguindo o sinal por toVueRef for grosso demais — veja a referência da API Vue.
O tipo de props de cada componente também é exportado: StoriesOverlayProps, StoriesUrlOverlayProps, StoriesRingProps, StoriesRingListProps, StoryHeaderProps, CanvasProgressBarProps, ImageStorySlideProps e VideoStorySlideProps.
Tipos
StoryItem, AuthorInfo, StoriesGroup
interface StoryItem {
/** Unique within its group. Stored viewed state keys on it. */
id: string;
mediaType: 'image' | 'video';
src: string;
/** Poster for a video, also used as the carousel card preview. */
poster?: string;
/** Overrides defaultImageDuration for this story, in milliseconds. */
duration?: number;
createdAt?: string | Date;
/** Media aspect ratio (width / height). */
aspectRatio?: number;
}
interface AuthorInfo {
/** Stable id. Viewed state and stable-id URLs key on it. */
id: string;
name: string;
avatar: string;
verified?: boolean;
}
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}Escopos dos slots
Todo tipo de escopo é exportado, então dá para anotar um slot onde o compilador não consegue inferir — HeaderSlotScope, FooterSlotScope, SlideSlotScope, NavigationSlotScope, ProgressBarSlotScope, LoadingSlotScope, ErrorSlotScope e GroupPreviewSlotScope, ao lado de StoriesApi:
interface HeaderSlotScope<T extends StoryItem = StoryItem> {
author: AuthorInfo;
story: T;
storyIndex: number;
isPaused: boolean;
isMuted: boolean;
/** True while the active story is a video — gate your sound button on it. */
isVideo: boolean;
groupIndex: number;
/** False for a neighbouring group's header with chrome-placement="group". */
isActive: boolean;
onToggleSound: () => void;
onTogglePause: () => void;
onClose: () => void;
}
interface FooterSlotScope<T extends StoryItem = StoryItem> {
author: AuthorInfo;
story: T;
storyIndex: number;
}
interface SlideSlotScope<T extends StoryItem = StoryItem> {
story: T;
index: number;
groupIndex: number;
/** False for the copy a carousel card draws. */
isActive: boolean;
size: [number, number];
activeGroupIndex: Signal<number>;
activeStoryIndex: Signal<number>;
/** Report the real media length, in milliseconds. */
onDurationReady: (durationMs: number) => void;
onReady: () => void;
onWaiting: () => void;
onError: () => void;
onEnded: () => void;
}
interface NavigationSlotScope {
onPrevStory: () => void;
onNextStory: () => void;
onPrevGroup: () => void;
onNextGroup: () => void;
}
interface ProgressBarSlotScope<T extends StoryItem = StoryItem> {
totalStories: number;
activeIndex: Signal<number>;
/** 0 to 1 for the active story. Bridge it with toVueRef. */
progress: Signal<number>;
group: StoriesGroup<T>;
groupIndex: number;
/** False: the signals hold still for a neighbouring group. */
isActive: boolean;
}
interface LoadingSlotScope<T extends StoryItem = StoryItem> {
story: T;
storyIndex: number;
groupIndex: number;
}
/** Same shape as LoadingSlotScope. */
interface ErrorSlotScope<T extends StoryItem = StoryItem> {
story: T;
storyIndex: number;
groupIndex: number;
}
interface GroupPreviewSlotScope<T extends StoryItem = StoryItem> {
group: StoriesGroup<T>;
groupIndex: number;
/** The story the group would open on, if it has one. */
story: T | undefined;
/** Negative left, positive right, 0 while sliding through the center. */
offset: number;
viewedCount: number;
onOpen: () => void;
}
interface StoriesApi {
nextStory: () => void;
prevStory: () => void;
nextGroup: () => void;
prevGroup: () => void;
goToGroup: (index: number) => void;
pause: () => void;
resume: () => void;
}Tipos de story personalizados
Estenda StoryItem com os seus campos e tipe os grupos com ele. StoriesOverlay e StoriesUrlOverlay são genéricos, como o player React: o tipo de story é inferido de groups, então grupos cujas stories não correspondem a StoryItem falham na checagem de tipos. No Vue os slots não conseguem carregar esse tipo, então os escopos ficam sem tipo até você anotá-los. Anote o escopo do slot para os campos extras continuarem visíveis dentro do template. Nada confere a anotação com groups, então mantenha os dois em sintonia:
<script setup lang="ts">
import { ref } from 'vue';
import {
StoriesOverlay,
type SlideSlotScope,
type StoriesGroup,
type StoryItem,
} from '@reelkit/vue-stories-player';
const isOpen = ref(false);
interface PromoStory extends StoryItem {
title?: string;
ctaHref?: string;
}
const groups: StoriesGroup<PromoStory>[] = [
{
author: { id: 'shop', name: 'Shop', avatar: '/shop.jpg' },
stories: [
{
id: 'sale',
mediaType: 'image',
src: '/sale.jpg',
title: 'Spring sale',
ctaHref: '/sale',
},
],
},
];
</script>
<template>
<StoriesOverlay v-model:is-open="isOpen" :groups="groups">
<!-- Annotate the scope with your own item type to keep the slot typed. -->
<template #slide="scope: SlideSlotScope<PromoStory>">
<article class="promo">
<h2>{{ scope.story.title }}</h2>
<a v-if="scope.story.ctaHref" :href="scope.story.ctaHref">Shop now</a>
</article>
</template>
</StoriesOverlay>
</template>Classes CSS
As classes são simples (não têm escopo), então uma folha de estilos carregada depois de @reelkit/vue-stories-player/styles.css pode sobrescrever qualquer uma. Elas são idênticas às de @reelkit/react-stories-player, então uma folha escrita para um binding serve para o outro. Para cores, tamanhos e z-index prefira as propriedades customizadas da seção Tema.
| Classe | Componente | Descrição |
|---|---|---|
.rk-stories-overlay | Overlay | Fundo fixo em tela cheia (cor, z-index) |
.rk-stories-overlay--carousel | Overlay | Overlay com o carrossel do desktop visível |
.rk-stories-overlay--sliding | Overlay | Deslize do carrossel em andamento; a tela do player fica escondida |
.rk-stories-swipe-wrapper | Overlay | Wrapper do deslizar para fechar (abriga os botões de navegação e a tela) |
.rk-stories-container | Overlay | Tela arredondada do story (posição, overflow) |
.rk-stories-ui-layer | Overlay | Contêiner da interface (cabeçalho, progresso). Dentro de cada slide de grupo com chrome-placement="group" |
.rk-stories-ui-layer--hidden | Overlay | Estado escondido da interface (alternado por hide-ui-on-pause) |
.rk-stories-error | Overlay | Estado de erro (ícone e texto centralizados) |
.rk-stories-error-text | Overlay | Texto da mensagem de erro |
.rk-stories-nav-btn | Navigation | Seta de avançar/voltar no desktop |
.rk-stories-progress-bar | CanvasProgressBar | Wrapper de posicionamento da barra em canvas |
.rk-stories-slide-wrapper | Group | Um grupo de stories (slide externo) |
.rk-stories-story | Story | Um story (raiz do slide interno) |
.rk-stories-header | StoryHeader | Barra do cabeçalho (avatar, nome, ações) |
.rk-stories-header--hidden | StoryHeader | Estado escondido do cabeçalho |
.rk-stories-header-avatar | StoryHeader | Imagem do avatar do autor |
.rk-stories-header-name | StoryHeader | Texto do nome do autor |
.rk-stories-header-verified | StoryHeader | Selo de verificado |
.rk-stories-header-time | StoryHeader | Texto de tempo relativo |
.rk-stories-header-actions | StoryHeader | Ações à direita (som, pausa, fechar) |
.rk-stories-header-btn | StoryHeader | Botão de ação do cabeçalho |
.rk-stories-header-btn--desktop | StoryHeader | Botão de pausa, exibido só em telas com mais de 768px de largura |
.rk-stories-header-spinner | StoryHeader | Indicador de carregamento |
.rk-stories-image | ImageStorySlide | Elemento do story em imagem |
.rk-stories-video | VideoStorySlide | Contêiner do story em vídeo |
.rk-stories-video-element | VideoStorySlide | O elemento <video> compartilhado |
.rk-stories-video-poster | VideoStorySlide | Poster do vídeo (some quando a reprodução começa) |
.rk-stories-video-poster--visible | VideoStorySlide | Estado do poster visível (antes da reprodução) |
.rk-stories-heart | HeartAnimation | Animação do coração no toque duplo |
.rk-stories-ring | StoriesRing | Anel do story (avatar com borda em gradiente animada) |
.rk-stories-ring--active | StoriesRing | Anel com stories por ver (anima) |
.rk-stories-ring-avatar | StoriesRing | Imagem do avatar dentro do anel |
.rk-stories-ring-list | StoriesRingList | Contêiner da lista horizontal de anéis |
.rk-stories-ring-list-item | StoriesRingList | Coluna com anel e nome |
.rk-stories-ring-list-name | StoriesRingList | Nome do autor sob cada anel |
.rk-stories-carousel | Carousel | Camada de cartões atrás do player |
.rk-stories-carousel--instant | Carousel | Primeiro quadro de um deslize, posicionado sem transição |
.rk-stories-card | Carousel | Um cartão posicionado |
.rk-stories-card--center | Carousel | Cartão na posição central durante um deslize |
.rk-stories-card--hidden | Carousel | Cartão além dos dois mostrados de cada lado |
.rk-stories-card-button | Carousel | Conteúdo padrão do cartão (o botão que abre o grupo) |
.rk-stories-card-image | Carousel | Imagem de prévia (poster ou imagem) |
.rk-stories-card-frame | Carousel | Um story sem poster nem imagem, desenhado pelo slot #slide no tamanho do player e reduzido |
.rk-stories-card-scrim | Carousel | Camada que escurece a prévia |
.rk-stories-card-info | Carousel | Coluna com anel, nome do autor e tempo |
.rk-stories-card-name | Carousel | Nome do autor |
.rk-stories-card-time | Carousel | Texto de tempo relativo |
Tema
Cada cor, tamanho, z-index e transição vive numa propriedade customizada de CSS, com o padrão em :root. Sobrescreva uma ou várias em :root, ou em qualquer ancestral do overlay, para trocar o tema sem tocar no código dos componentes. Os tokens são os mesmos de @reelkit/react-stories-player, então um tema passa de um binding para o outro.
| Token | Padrão | Controla |
|---|---|---|
--rk-stories-overlay-bg | #000 | Cor do fundo em tela cheia |
--rk-stories-overlay-z | 9999 | z-index do overlay |
--rk-stories-container-radius | 12px | Cantos arredondados da tela do story (desktop) |
--rk-stories-container-radius-mobile | 0 | Cantos da tela do story no celular, até 768px de largura |
--rk-stories-swipe-gap | 16px | Espaço entre os botões de navegação e a tela do story |
--rk-stories-top-shade-height | 120px | Altura do degradê superior atrás do cabeçalho |
--rk-stories-top-shade-bg | linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%) | Cor do degradê superior |
--rk-stories-ui-transition | 200ms | Duração do fade quando hide-ui-on-pause alterna |
--rk-stories-ui-z | 15 | z-index da camada de interface (barra de progresso e cabeçalho) |
--rk-stories-nav-size | 44px | Tamanho do botão de avançar/voltar no desktop |
--rk-stories-nav-bg | rgba(255, 255, 255, 0.1) | Fundo do botão de navegação no desktop |
--rk-stories-nav-bg-hover | rgba(255, 255, 255, 0.2) | Fundo do botão de navegação sob o cursor |
--rk-stories-nav-fg | rgba(255, 255, 255, 0.7) | Cor do ícone do botão de navegação |
--rk-stories-nav-fg-hover | #fff | Cor do ícone do botão sob o cursor |
--rk-stories-nav-transition | 150ms | Transição do botão de navegação sob o cursor no desktop |
--rk-stories-nav-z | 20 | z-index do botão de navegação no desktop |
--rk-stories-error-bg | linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) | Degradê de fundo do estado de erro |
--rk-stories-error-fg | rgba(255, 255, 255, 0.5) | Cor do ícone e do texto de erro |
--rk-stories-error-text-size | 13px | Tamanho da fonte da mensagem de erro |
--rk-stories-error-gap | 12px | Espaço entre o ícone de erro e o texto |
--rk-stories-error-z | 5 | z-index do estado de erro |
--rk-stories-video-bg | #000 | Fundo das bordas atrás do <video> |
--rk-stories-video-poster-transition | 200ms | Duração do fade do poster quando o vídeo começa |
--rk-stories-progress-bar-padding | 8px 8px 0 | Espaçamento em volta da barra de progresso |
--rk-stories-progress-bar-z | 10 | z-index da barra de progresso |
--rk-stories-header-top | 18px | Deslocamento vertical do cabeçalho a partir do topo do story |
--rk-stories-header-padding | 12px 16px | Espaçamento interno da linha do cabeçalho |
--rk-stories-header-z | 5 | z-index do cabeçalho |
--rk-stories-header-transition | 200ms | Duração do fade do cabeçalho quando ele se esconde |
--rk-stories-header-gap | 8px | Espaço entre o avatar, o nome e o tempo |
--rk-stories-header-avatar-size | 32px | Largura e altura do avatar |
--rk-stories-header-name-fg | #fff | Cor do nome do autor |
--rk-stories-header-name-size | 14px | Tamanho da fonte do nome do autor |
--rk-stories-header-name-weight | 600 | Peso da fonte do nome do autor |
--rk-stories-header-time-fg | rgba(255, 255, 255, 0.6) | Cor do texto de tempo relativo |
--rk-stories-header-time-size | 12px | Tamanho da fonte do tempo relativo |
--rk-stories-header-btn-fg | #fff | Cor dos ícones de ação do cabeçalho (fechar, mudo, pausa) |
--rk-stories-header-btn-padding | 4px | Espaçamento interno de um botão de ação do cabeçalho |
--rk-stories-header-actions-gap | 8px | Espaço entre os botões de ação do cabeçalho |
--rk-stories-header-spinner-size | 20px | Diâmetro do indicador de carregamento |
--rk-stories-header-spinner-fg | #fff | Cor do arco do indicador de carregamento |
--rk-stories-header-spinner-track | rgba(255, 255, 255, 0.3) | Cor do trilho do indicador de carregamento |
--rk-stories-header-spinner-duration | 0.8s | Duração da rotação do indicador de carregamento |
--rk-stories-heart-duration | 800ms | Duração da animação de entrada e saída do coração |
--rk-stories-heart-z | 20 | z-index do coração do toque duplo |
--rk-stories-ring-spin-duration | 4s | Duração da rotação de um anel com stories por ver |
--rk-stories-ring-active-scale | 0.95 | Escala do anel enquanto é pressionado |
--rk-stories-ring-active-gradient | conic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433) | O que um anel com stories por ver mostra: qualquer imagem ou cor CSS. Numa página clara, defina-o em .rk-stories-ring-list, não em :root, para o carrossel do player manter o seu |
--rk-stories-ring-viewed-color | rgba(255, 255, 255, 0.25) | O que um anel visto até o fim mostra, pensado para uma página escura. Numa página clara, defina-o em .rk-stories-ring-list, não em :root |
--rk-stories-ring-gradient | none | Interno: o anel o escreve a cada renderização e aponta para os dois tokens acima, a menos que o anel receba gradientColors ou viewedColor |
--rk-stories-ring-list-gap | 12px | Espaço entre os anéis da lista |
--rk-stories-ring-list-padding | 12px | Espaçamento interno ao redor da lista de anéis |
--rk-stories-ring-list-item-gap | 4px | Espaço entre um anel e o nome |
--rk-stories-ring-list-name-size | 12px | Tamanho da fonte do nome sob cada anel |
--rk-stories-card-bg | #262626 | Fundo do cartão, visível quando não há imagem de prévia |
--rk-stories-card-radius | 8px | Raio dos cantos do cartão |
--rk-stories-card-scrim | rgba(0, 0, 0, 0.45) | Escurecimento de um cartão lateral |
--rk-stories-card-fg | #fff | Cor do texto do cartão |
--rk-stories-card-name-size | 14px | Tamanho da fonte do nome no cartão |
--rk-stories-card-time-fg | rgba(255, 255, 255, 0.7) | Cor do texto de tempo no cartão |
--rk-stories-card-time-size | 13px | Tamanho da fonte do tempo no cartão |
--rk-stories-card-gap | 6px | Espaço entre anel, nome e tempo no cartão |
--rk-stories-card-transition | 300ms | Duração do deslize do carrossel |
/* Loaded after @reelkit/vue-stories-player/styles.css. Every token has a
default on :root, so override only what you want to change. The names
match @reelkit/react-stories-player, so a theme ports between bindings. */
:root {
--rk-stories-overlay-bg: #0f172a;
--rk-stories-container-radius: 24px;
--rk-stories-nav-bg: rgba(99, 102, 241, 0.35);
--rk-stories-nav-bg-hover: rgba(168, 85, 247, 0.65);
--rk-stories-nav-fg: #fff;
--rk-stories-header-name-fg: #fef3c7;
--rk-stories-header-time-fg: rgba(254, 243, 199, 0.7);
--rk-stories-ring-spin-duration: 2s;
--rk-stories-heart-duration: 600ms;
--rk-stories-card-bg: #1e293b;
--rk-stories-card-transition: 260ms;
}
/* Rings on a light page. Set on the ring list, not :root, so the rings of the
player's own carousel keep the colours made for its dark backdrop. */
.rk-stories-ring-list {
--rk-stories-ring-viewed-color: #d4d4d8;
}Acessibilidade
A raiz do overlay é um diálogo modal (role="dialog", aria-modal="true"). Defina aria-label para mudar o que o leitor de tela anuncia; o padrão é "Stories player".
O overlay captura o foco ao abrir e o devolve ao gatilho ao fechar. Tab e Shift+Tab circulam pelos elementos focáveis de dentro; um foco que escapa é trazido de volta. Feito com captureFocusForReturn e createFocusTrap, de @reelkit/core, reexportados por @reelkit/vue.
Com o carrossel do desktop, cada cartão padrão é um botão rotulado "Open stories by" mais o nome do autor. Os cartões vêm depois dos controles do player na ordem de tabulação e ficam fora dela enquanto um deslize acontece; o cartão que abre o seu grupo devolve o foco ao diálogo em vez de largá-lo na página.
Renderização no servidor
Fechado, o overlay não renderiza nada, e StoriesRingList renderiza todos os anéis como não vistos no servidor: o armazenamento do que foi visto só é lido depois da montagem, então a marcação do servidor bate com a primeira renderização no cliente. O quadro completo está no guia de SSR.
Atalhos de teclado
| Tecla | Ação |
|---|---|
ArrowLeft | Story anterior. No primeiro story de um grupo, abre o grupo anterior onde ele foi deixado; no primeiro grupo, não faz nada. |
ArrowRight | Próximo story. Depois do último story de um grupo, abre o próximo grupo; no último grupo, fecha o player. |
Escape | Fechar o player |