Angular Stories Player
Overlay de stories no estilo Instagram para Angular, com @reelkit/angular-stories-player. Usa o mesmo motor @reelkit/stories-core que os players de React e Vue, então o comportamento, as classes CSS e os tokens de tema são os mesmos.
Recursos
Instalação
npm install @reelkit/angular-stories-player @reelkit/angular lucide-angularRegistre a folha de estilos uma única vez:
// Anywhere that runs at startup — main.ts, app.config.ts, a component file.
import '@reelkit/angular-stories-player/styles.css';
// Or, if you keep CSS out of TypeScript, from your global stylesheet:
// @import '@reelkit/angular-stories-player/styles.css';
// Or register it in angular.json:
// "styles": ["node_modules/@reelkit/angular-stories-player/styles.css"]Ícones
O cabeçalho e as setas de navegação padrão usam lucide-angular para os ícones. Se preferir outra biblioteca de ícones, substitua-os pelos slots de template rkStoriesHeader e rkStoriesNavigation.
Início rápido
rk-stories-overlay renderiza o player em tela cheia. Combine-o com rk-stories-ring-list para as entradas no estilo do Instagram: um anel emite (selected) com o índice do seu grupo, que o host guarda como initialGroupIndex antes de definir isOpen. O player nunca se fecha sozinho; ele emite (closed), e o host volta isOpen para false.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
RkStoriesOverlayComponent,
RkStoriesRingListComponent,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
@Component({
selector: 'app-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups" (selected)="open($event)" />
<!-- The player reports a close instead of closing itself, so the host
decides: the ✕ button, a swipe down, Escape and the last story
finishing all arrive as (closed). -->
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
[initialGroupIndex]="groupIndex()"
(closed)="isOpen.set(false)"
/>
`,
})
export class FeedComponent {
protected readonly 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' }],
},
];
protected readonly isOpen = signal(false);
protected readonly groupIndex = signal(0);
protected open(index: number): void {
this.groupIndex.set(index);
this.isOpen.set(true);
}
}Onde o overlay é renderizado
Ao contrário dos players de React e Vue, que movem o overlay para o fim de document.body, o rk-stories-overlay é renderizado onde você o coloca e cobre a tela com position: fixed. Mantenha-o fora de qualquer ancestral com transform, perspective, filter ou will-change: transform: esse ancestral vira o bloco de contenção do overlay, então o player assume o tamanho dele em vez da viewport e é cortado pelo seu overflow. Um ancestral que cria o próprio contexto de empilhamento também limita o quanto --rk-stories-overlay-z consegue elevar o player. Em troca, os tokens --rk-stories-* definidos em qualquer ancestral chegam ao player.
Estado na URL
Ver demo ao vivo →RkStoriesUrlOverlayComponent é 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 createOverlayUrlState 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/angular-stories-player. Veja o guia de estado na URL e a Core API.
import { ChangeDetectionStrategy, Component, Injector, inject, runInInjectionContext, type OnInit } from '@angular/core';
import {
RkStoriesUrlOverlayComponent,
RkStoriesRingListComponent,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
import {
createOverlayUrlState,
urlIndexTwoAxisKey,
type TwoAxisPosition,
type UrlStateController,
} from '@reelkit/angular';
import { createRouterUrlAdapter } from '@reelkit/angular/ng-router-url-adapter';
@Component({
selector: 'app-url-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesUrlOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups" (selected)="open($event)" />
<rk-stories-url-overlay [controller]="stories" [groups]="groups" />
`,
})
export class UrlFeedComponent implements OnInit {
private readonly injector = inject(Injector);
protected groups: StoriesGroup[] = [];
protected stories!: UrlStateController<TwoAxisPosition>;
ngOnInit(): void {
// Both the controller and the router adapter need an injection context,
// and ngOnInit does not run in one, so borrow the component's.
this.stories = runInInjectionContext(this.injector, () =>
createOverlayUrlState({
param: 'story',
adapter: createRouterUrlAdapter(),
...urlIndexTwoAxisKey({
outerCount: () => this.groups.length,
innerCounts: () => this.groups.map((group) => group.stories.length),
}),
}),
) as UrlStateController<TwoAxisPosition>;
}
// Swiping only replaces the entry, so one back step always closes the
// player rather than walking back through every story.
protected open(groupIndex: number): void {
this.stories.set(`${groupIndex}.0`);
}
}- 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.
createRouterUrlAdapter, de@reelkit/angular/ng-router-url-adapter, é o adaptador pronto para o roteador do Angular; 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 por um id estável — outerCodec escreve o id na URL e outerLocator encontra onde ele está. Passe innerCodec junto com innerLocate e innerIdentify para fazer o mesmo com o story dentro do seu grupo; se você omitir os três, a metade do story continua sendo um índice local simples.
// ?story=alice.s3 — each axis addressed by a stable id, so a link survives
// the feed being reordered or paged.
import { urlStableIdKey } from '@reelkit/angular';
const key = urlIndexTwoAxisKey({
outerCodec: urlStableIdKey({ items: () => groups }).codec,
outerLocator: {
locate: (id) => groups.findIndex((group) => group.author.id === id),
identify: (index) => groups[index].author.id,
},
outerCount: () => groups.length,
innerCounts: () => groups.map((group) => group.stories.length),
innerCodec: urlStableIdKey({ items: () => [] }).codec,
innerLocate: (outer, id) =>
groups[outer].stories.findIndex((story) => story.id === id),
innerIdentify: (outer, index) => groups[outer].stories[index].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.
// locateAsync is asked only when locate misses — the group the link names is
// past what the feed has loaded. Page up to it, commit, and answer with its
// index; the story half is re-bounded against whichever group it settles on.
const key = urlIndexTwoAxisKey({
outerCount: () => this.groups().length,
innerCounts: () => this.groups().map((group) => group.stories.length),
outerLocator: {
locate: (index) => (index < this.groups().length ? index : null),
identify: (index) => index,
locateAsync: async (index) => {
const loaded = await this.loadUntilGroup(index);
if (!loaded) return null; // exhausted — the link names no group
this.groups.set(loaded); // commit — the player renders from this signal
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
createOverlayUrlStateestão na referência da API Angular, e o passo a passo está no guia do Angular.
Lembrar o que já foi visto
Os anéis mostram um gradiente até que o grupo seja assistido até o fim, e um grupo reabre na primeira story que o espectador ainda não viu. As duas coisas vêm de um único createStoriesViewedStateController, criado uma vez onde o feed vive e entregue à lista de anéis e ao player como viewed: eles leem o armazenamento, acompanham as mudanças e registram por conta própria o que é exibido. Uma entrada guardada identifica o autor e a story por id, então sobrevive a uma reordenação do feed, e o mesmo controller funciona em React e em Vue.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
RkStoriesOverlayComponent,
RkStoriesRingListComponent,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
@Component({
selector: 'app-remembering-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups()" [viewed]="viewed" (selected)="open($event)" />
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups()"
[initialGroupIndex]="groupIndex()"
[viewed]="viewed"
(closed)="isOpen.set(false)"
/>
`,
})
export class RememberingFeedComponent {
protected readonly groups = signal<StoriesGroup[]>([]);
// One controller for both: the rings mute as a user is watched to the end,
// and the player opens each user on their first unseen story. It reads the
// groups through the signal, so users paged in later are counted too.
protected readonly viewed = createStoriesViewedStateController({
storageKey: 'my-app-stories-seen',
groups: () => this.groups(),
});
protected readonly isOpen = signal(false);
protected readonly groupIndex = signal(0);
protected open(index: number): void {
this.groupIndex.set(index);
this.isOpen.set(true);
}
}- A story de abertura conta. A story que já está na tela é registrada como vista quando o player abre, então abrir um grupo de uma única story e fechar a marca como assistida.
- Avançar deslizando também retoma.
resumeStoryIndexé consultado para cada grupo alcançado pela primeira vez nesta sessão, incluindo aquele em que o player abre, a menos queinitialStoryIndexindique uma story explicitamente; um grupo já percorrido reabre onde foi deixado. Um inputresumeStoryIndexexplícito prevalece sobre a resposta do controller. - Um link continua prevalecendo. Com
rk-stories-url-overlay, o parâmetro decide onde o player abre, seja o que for que esteja guardado. Em todos os outros casos, quem decide é o callback de retomada. - Uma contagem é uma posição, não um total. Uma entrada indica a story mais distante alcançada, então acrescentar uma story a um grupo assistido acende o anel de novo, e remover uma do meio encurta a contagem.
- O armazenamento é plugável. Passe
storage: createSessionStorageAdapter()para esquecer ao fechar a aba, ou o seu próprioStorageAdapter. Duas abas abertas ficam sincronizadas pelo evento de storage do navegador. - Um player criado só quando abre lê tarde demais.
rk-stories-overlaylê o armazenamento assim que existe, ainda fechado, erk-stories-ring-listfaz o mesmo. Envolvido em@if (open), o overlay não tem esse momento: chameattachViewedState(() => this.viewed)no construtor do host, que fica conectado enquanto o host existir. As opções, e as peças de nível mais baixo com que isso é construído, estão na página Stories Core.
Carrossel no desktop
Defina desktopLayout="carousel" para dispor o player como o visualizador do Instagram na web: a story ativa fica no centro, no tamanho de sempre, e até dois grupos vizinhos ficam de cada lado como cards menores e escurecidos. Clicar em um card abre aquele grupo, e os cards deslizam enquanto o grupo muda. Passe o mesmo controller viewed que a lista de anéis recebe, e o card de um grupo assistido ganha o anel apagado.
// 'carousel' lays the neighbouring users out as preview cards either side of
// the active story, the way Instagram does on the web. A phone always shows
// the active story alone, whatever this says.
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
desktopLayout="carousel"
(closed)="isOpen.set(false)"
/>
`,
})
export class CarouselFeedComponent {}- Celulares mantêm o player simples. Até 768px de largura os cards não são renderizados e os swipes usam
groupTransition, como no layout padrão'single'. Redimensionar atravessando essa largura troca ao vivo. Enquanto os cards aparecem, os grupos deslizam em vez de girar, diga o que dissergroupTransition. - Cada card mostra onde o grupo vai abrir: a story em que ele foi deixado nesta sessão, ou senão o que
resumeStoryIndexretornar. Ele exibe o pôster do vídeo ou a imagem. Uma story sem nenhum dos dois, como uma story de texto sobre um gradiente, é desenhada pelo seu templaterkStoriesSlideno tamanho do player e reduzida; um vídeo sem pôster mostra o fundo do card. Essa prévia recebeisActive: false, e os seus callbacks de ciclo de vida não fazem nada, então ela não consegue controlar o timer da story na tela. Deixe o que um slide faz ao ser criado, como reprodução ou analytics, atrás deisActive. - O timer espera o slide. A story não inicia o timer, e
(storyViewed)não dispara para ela, até os cards terminarem de se mover. - Toda forma de trocar de grupo desliza: um clique em card,
StoriesApi.goToGroup,nextGroupeprevGroup, e as setas do teclado além do fim de um grupo. Um swipe por toque move o próprio player, então não ganha um deslize por cima. - Mudar o visual de um card é o slot
rkStoriesGroupPreview, mostrado em Cards do carrossel personalizados. O player continua posicionando, escalando e deslizando o card.
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 chromePlacement="group" para dar a cada grupo os seus, dentro do próprio slide, e os dois giram junto com o grupo, como no Instagram.
// 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.
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
chromePlacement="group"
(closed)="isOpen.set(false)"
/>
`,
})
export class GroupChromeFeedComponent {}- Um grupo vizinho mostra onde parou. Enquanto o player gira até ele, a barra e o cabeçalho mostram a 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 templates são renderizados para cada grupo na tela. Os contextos de
rkStoriesProgressBarerkStoriesHeadertrazemgroupIndexeisActive; para um grupo vizinhoisActiveéfalsee os sinais de progresso ficam parados. - Um template
rkStoriesHeaderfica 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 cards deslizam, então a escolha aparece quando o grupo já está aberto.
Slots de template
Oito diretivas ng-template substituem qualquer parte do player. Cada uma recebe um contexto tipado em que todo valor tem nome, os mesmos nomes dos slots de React e Vue, então let-author="author" pega o autor no slot de cabeçalho. O valor principal também é o implícito, então só let-author faz o mesmo. Um slot que você não fornecer mantém a apresentação padrão.
| Diretiva | Classe | Contexto | Descrição |
|---|---|---|---|
rkStoriesError | RkStoriesErrorDirective | { $implicit: story, story, storyIndex, groupIndex } | Substitui o painel "Content unavailable" embutido, desenhado sobre a story ativa |
rkStoriesFooter | RkStoriesFooterDirective | { $implicit: story, story, author, storyIndex } | Adiciona um rodapé sob a story ativa; o player não desenha nenhum próprio |
rkStoriesGroupPreview | RkStoriesGroupPreviewDirective | { $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen } | Preenche um card do carrossel no desktop; o player continua posicionando, escalando e deslizando o card |
rkStoriesHeader | RkStoriesHeaderDirective | { $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose } | Substitui o cabeçalho padrão (autor, pausa, som, fechar). Com chromePlacement="group" ele fica na área de swipe; veja Barra de progresso e cabeçalho em cada grupo. |
rkStoriesLoading | RkStoriesLoadingDirective | { $implicit: story, story, storyIndex, groupIndex } | Substitui o indicador de carregamento do cabeçalho enquanto a story ativa carrega |
rkStoriesNavigation | RkStoriesNavigationDirective | { onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } } | Substitui as duas setas do desktop |
rkStoriesProgressBar | RkStoriesProgressBarDirective | { $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive } | Substitui a barra de progresso em canvas. activeIndex e progress são sinais do core — conecte-os com toAngularSignal. |
rkStoriesSlide | RkStoriesSlideDirective | { $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded } | Substitui os slides padrão de imagem e vídeo |
import {
RkStoriesOverlayComponent,
RkStoriesHeaderDirective,
RkStoriesFooterDirective,
} from '@reelkit/angular-stories-player';
@Component({
imports: [
RkStoriesOverlayComponent,
RkStoriesHeaderDirective,
RkStoriesFooterDirective,
],
template: `
<rk-stories-overlay [isOpen]="isOpen()" [groups]="groups" (closed)="isOpen.set(false)">
<!-- The author is the implicit value; everything else is named. -->
<ng-template rkStoriesHeader let-author let-onClose="onClose">
<header class="my-header">
<img [src]="author.avatar" [alt]="author.name" />
<strong>{{ author.name }}</strong>
<button type="button" (click)="onClose()">Close</button>
</header>
</ng-template>
<!-- The player draws no footer of its own; this adds one. -->
<ng-template rkStoriesFooter let-story let-author="author">
<footer class="my-footer">Reply to {{ author.name }}</footer>
</ng-template>
</rk-stories-overlay>
`,
})
export class SlottedComponent {}STORIES_TEMPLATE_SLOT_DIRECTIVES exporta as oito classes como um único array, para um componente que importe várias.
Cada slot é renderizado pelo injetor do próprio player, então um componente desenhado dentro de um deles resolve os providers do player — um rk-video-story-slide em um template de slide encontra o mesmo estado de som que o botão do cabeçalho escreve.
Slides personalizados
import {
RkStoriesSlideDirective,
RkImageStorySlideComponent,
RkVideoStorySlideComponent,
} from '@reelkit/angular-stories-player';
// A slide template is projected content, so it cannot pick up the player's
// story type on its own — cast to your own feed's type where you know it.
@Component({
template: `
<rk-stories-overlay [isOpen]="isOpen()" [groups]="groups" (closed)="isOpen.set(false)">
<ng-template
rkStoriesSlide
let-story
let-groupIndex="groupIndex"
let-index="index"
let-activeGroupIndex="activeGroupIndex"
let-activeStoryIndex="activeStoryIndex"
let-onReady="onReady"
let-onError="onError"
let-onDurationReady="onDurationReady"
>
@if (story.mediaType === 'video') {
<!-- Every video plays through one shared element, which this slide
claims while both indexes point at it. -->
<rk-video-story-slide
[src]="story.src"
[poster]="story.poster"
[groupIndex]="groupIndex"
[storyIndex]="index"
[activeGroupIndex]="activeGroupIndex"
[activeStoryIndex]="activeStoryIndex"
(durationReady)="onDurationReady($event)"
(playbackStarted)="onReady()"
(failed)="onError()"
/>
} @else {
<rk-image-story-slide
[src]="story.src"
(loaded)="onReady()"
(failed)="onError()"
/>
}
</ng-template>
</rk-stories-overlay>
`,
})
export class CustomSlideComponent {}Um slide personalizado informa ao player como está o seu conteúdo por meio de callbacks no contexto de rkStoriesSlide. O player só age sobre eles para a story na tela; uma chamada vinda de qualquer outro slide é ignorada.
| Callback | Quando |
|---|---|
onReady | O conteúdo está pronto (imagem carregada, vídeo tocando). O timer de progresso começa, ou espera o fim do deslize do carrossel. |
onWaiting | O conteúdo trava (vídeo em buffer no meio da reprodução). O indicador de carregamento aparece e o timer pausa. |
onError | O conteúdo não carregou. O painel de erro aparece e o timer pausa. |
onDurationReady | Informa a duração real da mídia em milissegundos, por exemplo a partir dos metadados do vídeo, para o timer acompanhá-la. Uma duration definida na story prevalece sobre ela. |
onEnded | A mídia terminou sozinha, por exemplo um vídeo tocado até o fim. O player passa para a próxima story sem esperar o timer. |
rk-image-story-slide emite (loaded) e (failed), e rk-video-story-slide emite (playbackStarted), (buffering), (failed), (durationReady) e (finished), então um template de slide que os envolve só precisa repassar cada um ao callback correspondente.
Cache do pré-carregador
Os componentes embutidos RkImageStorySlideComponent e RkVideoStorySlideComponent pré-carregam a próxima story em segundo plano. Quando o espectador chega a uma story pré-carregada, o conteúdo aparece na hora, sem indicador de carregamento. Uma URL que falhou uma vez também fica lembrada, então reabrir uma story quebrada mostra o erro de imediato em vez de tentar de novo.
Navegação personalizada
// The four moves arrive as the implicit value, so `let-nav` is all a
// template needs. Supplying this replaces both built-in arrows.
<ng-template rkStoriesNavigation let-nav>
<button type="button" (click)="nav.onPrevGroup()">Previous user</button>
<button type="button" (click)="nav.onPrevStory()">Back</button>
<button type="button" (click)="nav.onNextStory()">Next</button>
<button type="button" (click)="nav.onNextGroup()">Next user</button>
</ng-template>Barra de progresso personalizada
// activeIndex and progress are core signals, the same ones the built-in
// canvas bar reads. Bridge them with toAngularSignal to follow the timer.
<ng-template
rkStoriesProgressBar
let-group
let-total="totalStories"
let-activeIndex="activeIndex"
let-progress="progress"
>
<app-my-progress-bar
[totalStories]="total"
[activeIndex]="activeIndex"
[progress]="progress"
/>
</ng-template>Carregamento e erro personalizados
// Both are scoped to the story on screen: the player reports one loading
// state, and these draw only over the slide the viewer is looking at.
<ng-template rkStoriesLoading let-story>
<div class="my-spinner">Loading {{ story.mediaType }}…</div>
</ng-template>
<ng-template rkStoriesError let-story>
<div class="my-error">That story could not be loaded.</div>
</ng-template>Cards do carrossel personalizados
// One side card of the desktop carousel. `offset` is negative on the left
// and positive on the right, and 0 while the card slides through the centre.
<ng-template
rkStoriesGroupPreview
let-group
let-story="story"
let-offset="offset"
let-viewedCount="viewedCount"
let-onOpen="onOpen"
>
<button type="button" class="my-card" (click)="onOpen()">
<img [src]="group.author.avatar" [alt]="group.author.name" />
<span>{{ group.author.name }}</span>
<span>{{ viewedCount }} / {{ group.stories.length }} seen</span>
</button>
</ng-template>Transições
groupTransition controla o efeito 3D ao trocar de autor. Importe as funções de transição de @reelkit/angular:
import { cubeTransition, flipTransition, fadeTransition } from '@reelkit/angular';
@Component({
template: `
<!-- Between users only. Stories inside one user always cross-fade. -->
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
[groupTransition]="flip"
[innerTransitionDuration]="200"
(closed)="isOpen.set(false)"
/>
`,
})
export class TransitionsComponent {
protected readonly flip = flipTransition;
}Toque duplo e curtidas
Um toque duplo reproduz uma animação de coração, como retorno visual imediato. (doubleTapped) emite o índice do grupo e da story para você guardar a curtida no seu próprio estado. O player não guarda nenhum estado de curtida.
// A double tap plays the heart over the story and reports which story it
// landed on. The heart draws itself; this is only the reporting half.
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
(doubleTapped)="like($event.groupIndex, $event.storyIndex)"
(closed)="isOpen.set(false)"
/>Personalizar a animação do coração
Ajuste a velocidade com o token --rk-stories-heart-duration (veja Temas). Para cor, tamanho ou para esconder o coração por completo, mire na classe .rk-stories-heart. RkHeartAnimationComponent é exportado para uso separado.
: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 embutida ainda não pode ser substituída por um slot. Mude o estilo com CSS, ou esconda-a com display: none e rode sua própria animação a partir de (doubleTapped). Se precisar de um slot para o coração, avise em GitHub Issues.
StoriesApi
Pegue a API em (apiReady) para controlar o player de forma imperativa. O player é reconstruído a cada abertura, então um handle guardado de uma abertura anterior fica obsoleto — use o mais recente.
import type { StoriesApi } from '@reelkit/angular-stories-player';
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
(apiReady)="api = $event"
(closed)="isOpen.set(false)"
/>
<button type="button" (click)="api?.nextGroup()">Skip this user</button>
`,
})
export class DrivenComponent {
// Handed over once the player is ready to take orders, and again on every
// open: the player is rebuilt each time, so keep the newest one.
protected api: StoriesApi | null = null;
}Métodos
| Método | Tipo | Descrição |
|---|---|---|
nextStory() | () => void | Avança para a próxima story dentro do grupo atual |
prevStory() | () => void | Volta para a story anterior dentro do grupo atual |
nextGroup() | () => void | Muda para o próximo grupo de autor |
prevGroup() | () => void | Muda para o grupo de autor anterior |
goToGroup(index) | (index: number) => void | Pula 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 |
Subcomponentes
Peças exportadas, para compor dentro dos slots ou usar por conta própria.
| Componente | O que é |
|---|---|
RkCanvasProgressBarComponent | A barra de progresso segmentada em canvas, desenhada direto dos sinais do timer. Cuida de uma janela deslizante para grupos com muitas stories. Com [live]="false" só desenha quando os sinais mudam e depois de um redimensionamento, sem loop de animação. Tamanhos e cores são inputs separados: gap, barHeight, minSegmentWidth, bgColor, fillColor. |
RkHeartAnimationComponent | O coração do toque duplo. Emite (completed) quando a animação termina, para o pai removê-lo. |
RkImageStorySlideComponent | Slide de imagem em tela cheia com object-fit: cover, informando carregamento e erro por outputs. |
RkStoriesRingComponent | Avatar circular com o anel em gradiente. Dois estados: se ainda há algo para ver ele gira, e quando tudo foi visto fica liso. Emite (clicked). |
RkStoriesRingListComponent | Linha horizontal de anéis com os nomes dos autores, um por grupo. Emite (selected) com o índice do grupo. |
RkStoryHeaderComponent | O cabeçalho padrão: avatar, nome, selo de verificado, tempo relativo, pausa, som, fechar e o indicador de carregamento. Os botões de pausa e som só aparecem com [showPauseButton] e [showSoundButton] definidos, como o player faz no próprio cabeçalho. |
RkVideoStorySlideComponent | Slide de vídeo apoiado no único elemento <video> compartilhado, para a continuidade do som no iOS. Cuida da reprodução automática, dos pôsteres e do aviso de duração. |
RkStoriesCarouselComponent também é exportado, junto com seu tipo CarouselSlide, mas o overlay é dono de todos os valores que ele desenha — use o slot rkStoriesGroupPreview para estilizar um card em vez de montar o carrossel por conta própria.
import {
RkStoriesRingComponent,
RkStoriesRingListComponent,
RkStoryHeaderComponent,
RkCanvasProgressBarComponent,
RkImageStorySlideComponent,
RkVideoStorySlideComponent,
RkHeartAnimationComponent,
} from '@reelkit/angular-stories-player';
// Each piece the player draws is exported on its own, so a page can build a
// ring row without the overlay, or a slot can rebuild a region out of the
// same parts the player uses.
@Component({
imports: [RkStoriesRingComponent],
template: `
<rk-stories-ring
[author]="author"
[totalStories]="5"
[viewedCount]="2"
[size]="68"
(clicked)="open()"
/>
`,
})
export class RingOnlyComponent {}Tipos de story personalizados
Estenda StoryItem com seus próprios campos e tipe os grupos com ele:
// The player is generic over the story type, so a feed can carry whatever
// extra fields a slide template needs.
interface PromoStory extends StoryItem {
title?: string;
ctaText?: string;
}
const groups: StoriesGroup<PromoStory>[] = [
{
author: { id: 'shop', name: 'Shop', avatar: '/shop.jpg' },
stories: [
{ id: 'p1', mediaType: 'image', src: '/promo.jpg', title: 'Flash Sale', ctaText: 'Shop now' },
],
},
];Reexportações
Tudo o que um consumidor precisa sai deste pacote, então nunca é necessário importar direto de @reelkit/core.
| De | Reexportado |
|---|---|
@reelkit/angular | SoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, e os tipos OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter |
@reelkit/stories-core | createStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, e os tipos de conteúdo StoryItem, AuthorInfo, StoriesGroup, MediaType |
attachViewedState também é exportado, para um componente que seja dono de um controller de estado de visto e queira mantê-lo ativo enquanto esse componente existir.
Inputs de RkStoriesOverlayComponent
rk-stories-overlay
| Input | Tipo | Padrão | Descrição |
|---|---|---|---|
ariaLabel | string | 'Stories player' | Nome acessível da região de diálogo; os leitores de tela o anunciam quando o overlay abre |
chromePlacement | '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. |
defaultImageDuration | number | 5000 | Duração do avanço automático de uma story de imagem, em milissegundos |
desktopLayout | 'single' | 'carousel' | 'single' | Layout no desktop. 'carousel' mostra os grupos vizinhos como cards de prévia ao lado da story ativa e desliza entre grupos. Celulares sempre mostram a story sozinha. O tipo é exportado como DesktopLayout. |
enableKeyboard | boolean | true | Ativa a navegação por teclado (setas esquerda/direita, Escape) |
groups | StoriesGroup<T>[] | required | Grupos de stories a exibir. Grupos ou stories adicionados com o player aberto são captados sem perder o ponto em que o espectador estava. |
groupTransition | TransitionTransformFn | cubeTransition | Transição entre autores. Ignorada enquanto o carrossel do desktop está visível, que desliza no lugar. |
hideUIOnPause | boolean | true | Esconde a barra de progresso e o cabeçalho enquanto um toque longo mantém a pausa |
initialGroupIndex | number | 0 | Índice (a partir de zero) do grupo com que o player abre |
initialStoryIndex | number | retomar, senão 0 | Índice (a partir de zero) da story com que o player abre. Indicar uma vence qualquer coisa lembrada; se você omitir, o grupo de abertura retoma como qualquer outro. |
innerTransitionDuration | number | 200 | Duração do fade entre stories de um mesmo autor, em milissegundos |
isOpen | boolean | required | Renderiza o player e trava a rolagem do body enquanto for true |
minSegmentWidth | number | 8 | Largura mínima de um segmento de progresso antes de a barra passar a rolar uma janela de segmentos, em pixels |
resumeStoryIndex | (groupIndex: number) => number | — | Com qual story um grupo abre na primeira vez que é alcançado. Só é consultado para um grupo ainda não visitado nesta abertura, e vence viewed. |
tapZoneSplit | number | 0.3 | Proporção da zona de toque (0–1). A parte esquerda volta, o resto avança. |
viewed | StoriesViewedStateController | — | O que o espectador já viu, de createStoriesViewedStateController(). Os grupos retomam na primeira story não vista, cada story exibida é registrada, e os cards do carrossel apagam o anel de um grupo visto até o fim. Passe o mesmo controller para rk-stories-ring-list. |
Cada slot também tem um input, para um componente que guarde o TemplateRef em vez de projetá-lo: slideTemplate, headerTemplate, footerTemplate, progressBarTemplate, navigationTemplate, loadingTemplate, errorTemplate e groupPreviewTemplate. Um template indicado assim vence outro projetado dentro do player.
Outputs de RkStoriesOverlayComponent
| Output | Carga | Descrição |
|---|---|---|
(apiReady) | StoriesApi | Emitido quando o player está pronto para receber ordens, e de novo a cada abertura |
(closed) | void | Emitido quando o espectador fecha o player. O player não se mexe; quem decide é o host. Em rk-stories-url-overlay o parâmetro já está limpo a essa altura. |
(doubleTapped) | { groupIndex, storyIndex } | Emitido em um toque duplo, junto com a animação do coração |
(groupChanged) | number | Emitido depois que o grupo ativo muda |
(paused) | void | Emitido em um toque longo, ou no botão de pausa do cabeçalho |
(resumed) | void | Emitido ao soltar, ou no botão de reprodução do cabeçalho |
(storyChanged) | { groupIndex, storyIndex } | Emitido depois que a story ativa muda |
(storyCompleted) | { groupIndex, storyIndex } | Emitido quando o timer de uma story se esgota |
(storyViewed) | { groupIndex, storyIndex } | Emitido quando uma story aparece na tela, incluindo a de abertura |
O player informa o fechamento em vez de se fechar sozinho, então (closed) cobre todas as saídas: o botão ✕, um deslize para baixo, Escape e o fim da última story do último grupo.
Inputs de RkStoriesUrlOverlayComponent
rk-stories-url-overlay
Aceita todos os inputs acima menos o trio de estado de abertura — isOpen, initialGroupIndex, initialStoryIndex — que vem do controller.
| Input | Tipo | Padrão | Descrição |
|---|---|---|---|
controller | UrlStateController<TwoAxisPosition> | required | Controller de createOverlayUrlState combinado com urlIndexTwoAxisKey. A posição dele — um objeto { outer, inner } — decide se o player está aberto e por onde ele abre; o overlay a reescreve a cada navegação e ao fechar. |
Interfaces StoryItem, AuthorInfo e StoriesGroup
import type {
StoryItem,
AuthorInfo,
StoriesGroup,
StoriesApi,
DesktopLayout,
} from '@reelkit/angular-stories-player';
interface StoryItem {
id: string;
mediaType: 'image' | 'video';
src: string;
poster?: string;
duration?: number;
aspectRatio?: number;
createdAt?: string | Date;
}
interface AuthorInfo {
id: string;
name: string;
avatar: string;
verified?: boolean;
}
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}Tipos de contexto dos slots
Todos os tipos de contexto são exportados, para anotar um slot onde o compilador não consegue inferir — StoriesSlideContext, StoriesHeaderContext, StoriesFooterContext, StoriesProgressBarContext, StoriesNavigationContext, StoriesLoadingContext, StoriesErrorContext e StoriesGroupPreviewContext, junto com StoriesNavigationActions, DesktopLayout, ChromePlacement e StoriesApi:
// Every slot context in one place. Every value is named, by the names the
// react and vue slots use; the main one is also what `let-x` with no name
// binds.
import type {
StoriesSlideContext,
StoriesHeaderContext,
StoriesFooterContext,
StoriesProgressBarContext,
StoriesNavigationContext,
StoriesGroupPreviewContext,
StoriesLoadingContext,
StoriesErrorContext,
} from '@reelkit/angular-stories-player';Classes CSS
Os nomes de classe são simples (sem escopo), então uma folha de estilos carregada depois de @reelkit/angular-stories-player/styles.css pode sobrescrever qualquer um deles. São idênticos aos de @reelkit/react-stories-player e @reelkit/vue-stories-player, então uma folha escrita para um binding serve para os outros. Para cores, tamanhos e z-index, prefira as propriedades personalizadas em Temas.
| 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 deslize para fechar (abriga as setas e a tela) |
.rk-stories-container | Overlay | Tela arredondada da story (posição, overflow) |
.rk-stories-ui-layer | Overlay | Uma camada de interface sobre o player (cabeçalho, progresso, corações). Com chromePlacement="group", uma dentro de cada slide de grupo (cabeçalho, progresso), e os corações ficam direto no player |
.rk-stories-ui-layer--hidden | Overlay | Interface escondida (alternada por hideUIOnPause) |
.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 | Overlay | Seta de anterior/próximo no desktop |
.rk-stories-progress-bar | ProgressBar | Wrapper que posiciona a barra de progresso em canvas |
.rk-stories-slide-wrapper | Overlay | Um grupo de stories (slide externo) |
.rk-stories-story | Overlay | Uma única story (raiz do slide interno) |
.rk-stories-image | ImageStorySlide | Imagem em tela cheia (object-fit: cover) |
.rk-stories-video | VideoStorySlide | Contêiner do slide de vídeo |
.rk-stories-video-element | VideoStorySlide | O elemento <video> compartilhado |
.rk-stories-video-poster | VideoStorySlide | Pôster, visível até a reprodução começar |
.rk-stories-video-poster--visible | VideoStorySlide | Estado de pôster visível (antes da reprodução) |
.rk-stories-heart | HeartAnimation | O coração do toque duplo |
.rk-stories-header | StoryHeader | Linha do cabeçalho (avatar, nome, tempo, controles) |
.rk-stories-header--hidden | StoryHeader | Estado de cabeçalho escondido (input visible definido como false) |
.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 | Contêiner do selo de verificado |
.rk-stories-header-time | StoryHeader | Texto de tempo relativo |
.rk-stories-header-actions | StoryHeader | Controles do lado direito (som, pausa, fechar) |
.rk-stories-header-btn | StoryHeader | Botões de pausa, som e fechar |
.rk-stories-header-btn--desktop | StoryHeader | O botão de pausa, exibido só com mais de 768px de largura |
.rk-stories-header-spinner | StoryHeader | Indicador de carregamento enquanto a story ativa carrega |
.rk-stories-ring | StoriesRing | Avatar circular com o anel em gradiente |
.rk-stories-ring--active | StoriesRing | Anel com stories por ver (gradiente girando) |
.rk-stories-ring-avatar | StoriesRing | A imagem do avatar dentro do anel |
.rk-stories-ring-list | StoriesRingList | Linha horizontal de anéis |
.rk-stories-ring-list-item | StoriesRingList | Coluna com o anel e o nome |
.rk-stories-ring-list-name | StoriesRingList | Nome do autor abaixo de cada anel |
.rk-stories-carousel | Carousel | Camada de cards de prévia atrás do player |
.rk-stories-carousel--instant | Carousel | Primeiro quadro de um deslize, disposto sem transição |
.rk-stories-card | Carousel | Um card de prévia posicionado |
.rk-stories-card--center | Carousel | Card na posição central durante um deslize |
.rk-stories-card--hidden | Carousel | Card além dos dois mostrados de cada lado |
.rk-stories-card-button | Carousel | Conteúdo padrão do card (o botão que abre o grupo) |
.rk-stories-card-image | Carousel | A imagem de prévia do card (pôster ou imagem) |
.rk-stories-card-frame | Carousel | Um template de slide desenhado como prévia do card, em escala reduzida |
.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 |
Temas
Cada cor, tamanho, duração e z-index é uma propriedade personalizada --rk-stories-* em :root, então mudar a marca é uma folha de estilos e não uma mudança de componente. Os tokens são compartilhados com os players de React e Vue.
| Token | Padrão | O que controla |
|---|---|---|
--rk-stories-overlay-bg | #000 | Cor do fundo em tela cheia |
--rk-stories-overlay-z | 9999 | z-index do overlay |
--rk-stories-swipe-gap | 16px | Espaço entre as setas de navegação e a tela da story |
--rk-stories-container-radius | 12px | Cantos arredondados da tela da story (desktop) |
--rk-stories-container-radius-mobile | 0 | Cantos da tela da story no celular |
--rk-stories-ui-z | 15 | z-index da camada de interface (cabeçalho, barra de progresso) |
--rk-stories-ui-transition | 200ms | Duração do fade quando hideUIOnPause alterna |
--rk-stories-top-shade-height | 120px | Altura do gradiente 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 gradiente superior |
--rk-stories-error-z | 5 | z-index do painel de erro |
--rk-stories-error-bg | linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) | Gradiente 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-gap | 12px | Espaço entre o ícone de erro e o texto |
--rk-stories-error-text-size | 13px | Tamanho da fonte da mensagem de erro |
--rk-stories-nav-z | 20 | z-index das setas de navegação no desktop |
--rk-stories-nav-size | 44px | Tamanho das setas de anterior/próximo no desktop |
--rk-stories-nav-bg | rgba(255, 255, 255, 0.1) | Fundo das setas de navegação no desktop |
--rk-stories-nav-bg-hover | rgba(255, 255, 255, 0.2) | Fundo das setas sob o cursor |
--rk-stories-nav-fg | rgba(255, 255, 255, 0.7) | Cor do ícone das setas |
--rk-stories-nav-fg-hover | #fff | Cor do ícone das setas sob o cursor |
--rk-stories-nav-transition | 150ms | Transição das setas sob o cursor no desktop |
--rk-stories-video-bg | #000 | Fundo das faixas atrás do <video> |
--rk-stories-video-poster-transition | 200ms | Duração do fade do pôster quando o vídeo começa |
--rk-stories-progress-bar-z | 10 | z-index da barra de progresso |
--rk-stories-progress-bar-padding | 8px 8px 0 | Espaço em volta da barra de progresso |
--rk-stories-card-bg | #262626 | Fundo do card do carrossel, visível quando não há quadro de prévia |
--rk-stories-card-radius | 8px | Raio dos cantos do card do carrossel |
--rk-stories-card-scrim | rgba(0, 0, 0, 0.45) | Escurecimento sobre um card lateral |
--rk-stories-card-fg | #fff | Cor do texto do card |
--rk-stories-card-name-size | 14px | Tamanho da fonte do nome do autor no card |
--rk-stories-card-time-fg | rgba(255, 255, 255, 0.7) | Cor do tempo relativo no card |
--rk-stories-card-time-size | 13px | Tamanho da fonte do tempo relativo no card |
--rk-stories-card-gap | 6px | Espaço entre anel, nome e tempo no card |
--rk-stories-card-transition | 300ms | Duração do deslize do carrossel |
--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 em volta 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 abaixo de cada anel |
--rk-stories-header-z | 5 | z-index do cabeçalho |
--rk-stories-header-top | 18px | Deslocamento vertical do cabeçalho a partir do topo da story |
--rk-stories-header-padding | 12px 16px | Espaçamento interno da linha do cabeçalho |
--rk-stories-header-gap | 8px | Espaço entre os itens da linha do cabeçalho |
--rk-stories-header-transition | 200ms | Duração do fade do cabeçalho quando ele se esconde |
--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-actions-gap | 8px | Espaço entre os botões do cabeçalho |
--rk-stories-header-btn-fg | #fff | Cor dos ícones de ação do cabeçalho (fechar, som, pausa) |
--rk-stories-header-btn-padding | 4px | Espaçamento interno dos botões do cabeçalho |
--rk-stories-header-spinner-size | 20px | Tamanho 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-fg | #fff | Cor do arco do indicador de carregamento |
--rk-stories-header-spinner-duration | 0.8s | Uma rotação do indicador de carregamento |
--rk-stories-heart-z | 20 | z-index do coração do toque duplo |
--rk-stories-heart-duration | 800ms | Duração da animação de entrada e saída do coração |
Cole o trecho abaixo em uma folha de estilos carregada depois de @reelkit/angular-stories-player/styles.css.
/* Loaded after @reelkit/angular-stories-player/styles.css. Every token has a
default on :root, so override only what you want to change. The names
match the React and Vue players, 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"). Use ariaLabel 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 percorrem os elementos focáveis de dentro, dando a volta nas duas pontas; o foco que escapa (clique fora, foco programático) é puxado de volta. A rolagem do body fica travada enquanto o player está aberto. Implementado com captureFocusForReturn e createFocusTrap de @reelkit/core, reexportados por @reelkit/angular.
Com o carrossel do desktop, cada card padrão é um botão rotulado "Open stories by" e o nome do autor. Os cards vêm depois dos controles do player na ordem de tabulação, e saem dela enquanto um deslize acontece; um card que abre o seu grupo devolve o foco ao diálogo em vez de largá-lo na página.
Renderização no servidor
O player não renderiza nada até estar aberto e não lê nenhuma API do navegador durante a construção, então é seguro com Angular SSR. O estado de visto é lido depois da montagem, e não durante a renderização, e é isso que mantém o render do servidor e o primeiro render do cliente de acordo.
Atalhos de teclado
| Tecla | Ação |
|---|---|
ArrowLeft | Story anterior. Na primeira story de um grupo, o grupo anterior, aberto onde foi deixado; no primeiro grupo, nada. |
ArrowRight | Próxima story. Na última story de um grupo, o próximo grupo; no último grupo, o player fecha. |
Escape | Fechar o player |