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.

Ver demo ao vivo →

Recursos

Navegação aninhada
Toque para passar stories, deslize para trocar de autor
Stories em vídeo
Reprodução automática com controle de som
Avanço automático
Duração configurável por story
Transições 3D
Cubo, flip, fade, zoom, slide
Barra de progresso
Progresso segmentado desenhado em canvas
Imagem e vídeo
Lida com os dois tipos de mídia
Slots de template
Oito regiões com ng-template
Curtida com toque duplo
Animação de coração ao tocar duas vezes
Navegação no desktop
Setas laterais em telas grandes
Anéis de story
Avatares com anel no estilo Instagram
Tipos genéricos
Estenda StoryItem com seus próprios campos
Tokens de tema
Mude a marca só com --rk-stories-*
Estado na URL
Links compartilháveis ?story=grupo.story
Estado de visto
Anéis vistos e retomada sobrevivem a um reload

Instalação

bash
npm install @reelkit/angular-stories-player @reelkit/angular lucide-angular

Registre a folha de estilos uma única vez:

typescript
// 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.

typescript
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.

typescript
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.

typescript
// ?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.

typescript
// 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 locateAsync está pendente, o player fica fechado e o parâmetro intacto, então o link direto sobrevive à busca. Um null ou 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 createOverlayUrlState estã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.

typescript
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 que initialStoryIndex indique uma story explicitamente; um grupo já percorrido reabre onde foi deixado. Um input resumeStoryIndex explí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óprio StorageAdapter. Duas abas abertas ficam sincronizadas pelo evento de storage do navegador.
  • Um player criado só quando abre lê tarde demais. rk-stories-overlay lê o armazenamento assim que existe, ainda fechado, e rk-stories-ring-list faz o mesmo. Envolvido em @if (open), o overlay não tem esse momento: chame attachViewedState(() => 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.

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.

typescript
// '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 disser groupTransition.
  • Cada card mostra onde o grupo vai abrir: a story em que ele foi deixado nesta sessão, ou senão o que resumeStoryIndex retornar. 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 template rkStoriesSlide no tamanho do player e reduzida; um vídeo sem pôster mostra o fundo do card. Essa prévia recebe isActive: 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 de isActive.
  • 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, nextGroup e prevGroup, 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.

typescript
// 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 rkStoriesProgressBar e rkStoriesHeader trazem groupIndex e isActive; para um grupo vizinho isActive é false e os sinais de progresso ficam parados.
  • Um template rkStoriesHeader fica na área de swipe. Um toque nele troca de story, a menos que caia em um button, um link ou um elemento com role="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.

DiretivaClasseContextoDescrição
rkStoriesErrorRkStoriesErrorDirective{ $implicit: story, story, storyIndex, groupIndex }Substitui o painel "Content unavailable" embutido, desenhado sobre a story ativa
rkStoriesFooterRkStoriesFooterDirective{ $implicit: story, story, author, storyIndex }Adiciona um rodapé sob a story ativa; o player não desenha nenhum próprio
rkStoriesGroupPreviewRkStoriesGroupPreviewDirective{ $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen }Preenche um card do carrossel no desktop; o player continua posicionando, escalando e deslizando o card
rkStoriesHeaderRkStoriesHeaderDirective{ $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.
rkStoriesLoadingRkStoriesLoadingDirective{ $implicit: story, story, storyIndex, groupIndex }Substitui o indicador de carregamento do cabeçalho enquanto a story ativa carrega
rkStoriesNavigationRkStoriesNavigationDirective{ onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } }Substitui as duas setas do desktop
rkStoriesProgressBarRkStoriesProgressBarDirective{ $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.
rkStoriesSlideRkStoriesSlideDirective{ $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }Substitui os slides padrão de imagem e vídeo
typescript
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

typescript
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.

CallbackQuando
onReadyO conteúdo está pronto (imagem carregada, vídeo tocando). O timer de progresso começa, ou espera o fim do deslize do carrossel.
onWaitingO conteúdo trava (vídeo em buffer no meio da reprodução). O indicador de carregamento aparece e o timer pausa.
onErrorO conteúdo não carregou. O painel de erro aparece e o timer pausa.
onDurationReadyInforma 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.
onEndedA 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

typescript
// 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

typescript
// 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

typescript
// 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>
typescript
// 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:

typescript
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.

typescript
// 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.

css
: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.

typescript
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étodoTipoDescrição
nextStory()() => voidAvança para a próxima story dentro do grupo atual
prevStory()() => voidVolta para a story anterior dentro do grupo atual
nextGroup()() => voidMuda para o próximo grupo de autor
prevGroup()() => voidMuda para o grupo de autor anterior
goToGroup(index)(index: number) => voidPula para um grupo específico pelo índice
pause()() => voidPausa o avanço automático e o timer de progresso
resume()() => voidRetoma 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.

ComponenteO que é
RkCanvasProgressBarComponentA 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.
RkHeartAnimationComponentO coração do toque duplo. Emite (completed) quando a animação termina, para o pai removê-lo.
RkImageStorySlideComponentSlide de imagem em tela cheia com object-fit: cover, informando carregamento e erro por outputs.
RkStoriesRingComponentAvatar 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).
RkStoriesRingListComponentLinha horizontal de anéis com os nomes dos autores, um por grupo. Emite (selected) com o índice do grupo.
RkStoryHeaderComponentO 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.
RkVideoStorySlideComponentSlide 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.

typescript
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:

typescript
// 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.

DeReexportado
@reelkit/angularSoundStateService, 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-corecreateStoriesViewedStateController, 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

InputTipoPadrãoDescrição
ariaLabelstring'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.
defaultImageDurationnumber5000Duraçã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.
enableKeyboardbooleantrueAtiva a navegação por teclado (setas esquerda/direita, Escape)
groupsStoriesGroup<T>[]requiredGrupos de stories a exibir. Grupos ou stories adicionados com o player aberto são captados sem perder o ponto em que o espectador estava.
groupTransitionTransitionTransformFncubeTransitionTransição entre autores. Ignorada enquanto o carrossel do desktop está visível, que desliza no lugar.
hideUIOnPausebooleantrueEsconde a barra de progresso e o cabeçalho enquanto um toque longo mantém a pausa
initialGroupIndexnumber0Índice (a partir de zero) do grupo com que o player abre
initialStoryIndexnumberretomar, 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.
innerTransitionDurationnumber200Duração do fade entre stories de um mesmo autor, em milissegundos
isOpenbooleanrequiredRenderiza o player e trava a rolagem do body enquanto for true
minSegmentWidthnumber8Largura 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.
tapZoneSplitnumber0.3Proporção da zona de toque (0–1). A parte esquerda volta, o resto avança.
viewedStoriesViewedStateController—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

OutputCargaDescrição
(apiReady)StoriesApiEmitido quando o player está pronto para receber ordens, e de novo a cada abertura
(closed)voidEmitido 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)numberEmitido depois que o grupo ativo muda
(paused)voidEmitido em um toque longo, ou no botão de pausa do cabeçalho
(resumed)voidEmitido 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.

InputTipoPadrãoDescrição
controllerUrlStateController<TwoAxisPosition>requiredController 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

typescript
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:

typescript
// 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.

ClasseComponenteDescrição
.rk-stories-overlayOverlayFundo fixo em tela cheia (cor, z-index)
.rk-stories-overlay--carouselOverlayOverlay com o carrossel do desktop visível
.rk-stories-overlay--slidingOverlayDeslize do carrossel em andamento; a tela do player fica escondida
.rk-stories-swipe-wrapperOverlayWrapper do deslize para fechar (abriga as setas e a tela)
.rk-stories-containerOverlayTela arredondada da story (posição, overflow)
.rk-stories-ui-layerOverlayUma 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--hiddenOverlayInterface escondida (alternada por hideUIOnPause)
.rk-stories-errorOverlayEstado de erro (ícone e texto centralizados)
.rk-stories-error-textOverlayTexto da mensagem de erro
.rk-stories-nav-btnOverlaySeta de anterior/próximo no desktop
.rk-stories-progress-barProgressBarWrapper que posiciona a barra de progresso em canvas
.rk-stories-slide-wrapperOverlayUm grupo de stories (slide externo)
.rk-stories-storyOverlayUma única story (raiz do slide interno)
.rk-stories-imageImageStorySlideImagem em tela cheia (object-fit: cover)
.rk-stories-videoVideoStorySlideContêiner do slide de vídeo
.rk-stories-video-elementVideoStorySlideO elemento <video> compartilhado
.rk-stories-video-posterVideoStorySlidePôster, visível até a reprodução começar
.rk-stories-video-poster--visibleVideoStorySlideEstado de pôster visível (antes da reprodução)
.rk-stories-heartHeartAnimationO coração do toque duplo
.rk-stories-headerStoryHeaderLinha do cabeçalho (avatar, nome, tempo, controles)
.rk-stories-header--hiddenStoryHeaderEstado de cabeçalho escondido (input visible definido como false)
.rk-stories-header-avatarStoryHeaderImagem do avatar do autor
.rk-stories-header-nameStoryHeaderTexto do nome do autor
.rk-stories-header-verifiedStoryHeaderContêiner do selo de verificado
.rk-stories-header-timeStoryHeaderTexto de tempo relativo
.rk-stories-header-actionsStoryHeaderControles do lado direito (som, pausa, fechar)
.rk-stories-header-btnStoryHeaderBotões de pausa, som e fechar
.rk-stories-header-btn--desktopStoryHeaderO botão de pausa, exibido só com mais de 768px de largura
.rk-stories-header-spinnerStoryHeaderIndicador de carregamento enquanto a story ativa carrega
.rk-stories-ringStoriesRingAvatar circular com o anel em gradiente
.rk-stories-ring--activeStoriesRingAnel com stories por ver (gradiente girando)
.rk-stories-ring-avatarStoriesRingA imagem do avatar dentro do anel
.rk-stories-ring-listStoriesRingListLinha horizontal de anéis
.rk-stories-ring-list-itemStoriesRingListColuna com o anel e o nome
.rk-stories-ring-list-nameStoriesRingListNome do autor abaixo de cada anel
.rk-stories-carouselCarouselCamada de cards de prévia atrás do player
.rk-stories-carousel--instantCarouselPrimeiro quadro de um deslize, disposto sem transição
.rk-stories-cardCarouselUm card de prévia posicionado
.rk-stories-card--centerCarouselCard na posição central durante um deslize
.rk-stories-card--hiddenCarouselCard além dos dois mostrados de cada lado
.rk-stories-card-buttonCarouselConteúdo padrão do card (o botão que abre o grupo)
.rk-stories-card-imageCarouselA imagem de prévia do card (pôster ou imagem)
.rk-stories-card-frameCarouselUm template de slide desenhado como prévia do card, em escala reduzida
.rk-stories-card-scrimCarouselCamada que escurece a prévia
.rk-stories-card-infoCarouselColuna com anel, nome do autor e tempo
.rk-stories-card-nameCarouselNome do autor
.rk-stories-card-timeCarouselTexto 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.

TokenPadrãoO que controla
--rk-stories-overlay-bg#000Cor do fundo em tela cheia
--rk-stories-overlay-z9999z-index do overlay
--rk-stories-swipe-gap16pxEspaço entre as setas de navegação e a tela da story
--rk-stories-container-radius12pxCantos arredondados da tela da story (desktop)
--rk-stories-container-radius-mobile0Cantos da tela da story no celular
--rk-stories-ui-z15z-index da camada de interface (cabeçalho, barra de progresso)
--rk-stories-ui-transition200msDuração do fade quando hideUIOnPause alterna
--rk-stories-top-shade-height120pxAltura do gradiente superior atrás do cabeçalho
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Cor do gradiente superior
--rk-stories-error-z5z-index do painel de erro
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Gradiente de fundo do estado de erro
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Cor do ícone e do texto de erro
--rk-stories-error-gap12pxEspaço entre o ícone de erro e o texto
--rk-stories-error-text-size13pxTamanho da fonte da mensagem de erro
--rk-stories-nav-z20z-index das setas de navegação no desktop
--rk-stories-nav-size44pxTamanho das setas de anterior/próximo no desktop
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Fundo das setas de navegação no desktop
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Fundo das setas sob o cursor
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Cor do ícone das setas
--rk-stories-nav-fg-hover#fffCor do ícone das setas sob o cursor
--rk-stories-nav-transition150msTransição das setas sob o cursor no desktop
--rk-stories-video-bg#000Fundo das faixas atrás do <video>
--rk-stories-video-poster-transition200msDuração do fade do pôster quando o vídeo começa
--rk-stories-progress-bar-z10z-index da barra de progresso
--rk-stories-progress-bar-padding8px 8px 0Espaço em volta da barra de progresso
--rk-stories-card-bg#262626Fundo do card do carrossel, visível quando não há quadro de prévia
--rk-stories-card-radius8pxRaio dos cantos do card do carrossel
--rk-stories-card-scrimrgba(0, 0, 0, 0.45)Escurecimento sobre um card lateral
--rk-stories-card-fg#fffCor do texto do card
--rk-stories-card-name-size14pxTamanho da fonte do nome do autor no card
--rk-stories-card-time-fgrgba(255, 255, 255, 0.7)Cor do tempo relativo no card
--rk-stories-card-time-size13pxTamanho da fonte do tempo relativo no card
--rk-stories-card-gap6pxEspaço entre anel, nome e tempo no card
--rk-stories-card-transition300msDuração do deslize do carrossel
--rk-stories-ring-spin-duration4sDuração da rotação de um anel com stories por ver
--rk-stories-ring-active-scale0.95Escala do anel enquanto é pressionado
--rk-stories-ring-active-gradientconic-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-colorrgba(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-gradientnoneInterno: 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-gap12pxEspaço entre os anéis da lista
--rk-stories-ring-list-padding12pxEspaçamento interno em volta da lista de anéis
--rk-stories-ring-list-item-gap4pxEspaço entre um anel e o nome
--rk-stories-ring-list-name-size12pxTamanho da fonte do nome abaixo de cada anel
--rk-stories-header-z5z-index do cabeçalho
--rk-stories-header-top18pxDeslocamento vertical do cabeçalho a partir do topo da story
--rk-stories-header-padding12px 16pxEspaçamento interno da linha do cabeçalho
--rk-stories-header-gap8pxEspaço entre os itens da linha do cabeçalho
--rk-stories-header-transition200msDuração do fade do cabeçalho quando ele se esconde
--rk-stories-header-avatar-size32pxLargura e altura do avatar
--rk-stories-header-name-fg#fffCor do nome do autor
--rk-stories-header-name-size14pxTamanho da fonte do nome do autor
--rk-stories-header-name-weight600Peso da fonte do nome do autor
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Cor do texto de tempo relativo
--rk-stories-header-time-size12pxTamanho da fonte do tempo relativo
--rk-stories-header-actions-gap8pxEspaço entre os botões do cabeçalho
--rk-stories-header-btn-fg#fffCor dos ícones de ação do cabeçalho (fechar, som, pausa)
--rk-stories-header-btn-padding4pxEspaçamento interno dos botões do cabeçalho
--rk-stories-header-spinner-size20pxTamanho do indicador de carregamento
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)Cor do trilho do indicador de carregamento
--rk-stories-header-spinner-fg#fffCor do arco do indicador de carregamento
--rk-stories-header-spinner-duration0.8sUma rotação do indicador de carregamento
--rk-stories-heart-z20z-index do coração do toque duplo
--rk-stories-heart-duration800msDuraçã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.

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

TeclaAção
ArrowLeftStory anterior. Na primeira story de um grupo, o grupo anterior, aberto onde foi deixado; no primeiro grupo, nada.
ArrowRightPróxima story. Na última story de um grupo, o próximo grupo; no último grupo, o player fecha.
EscapeFechar o player