Angular Stories Player

Overlay de stories al estilo Instagram para Angular, con @reelkit/angular-stories-player. Usa el mismo motor @reelkit/stories-core que los players de React y Vue, así que el comportamiento, las clases CSS y los tokens de tema son los mismos.

Ver demo en vivo →

Características

Navegación anidada
Toca para pasar stories, desliza para cambiar de autor
Stories en vídeo
Reproducción automática con control de sonido
Avance automático
Duración configurable por story
Transiciones 3D
Cubo, volteo, fundido, zoom, deslizamiento
Barra de progreso
Progreso segmentado dibujado en canvas
Imagen y vídeo
Admite ambos tipos de medio
Slots de plantilla
Ocho regiones con ng-template
Doble toque
Animación de corazón al tocar dos veces
Navegación en escritorio
Flechas laterales en pantallas grandes
Anillos de story
Avatares con anillo al estilo Instagram
Tipos genéricos
Extiende StoryItem con tus propios campos
Tokens de tema
Cambia la marca solo con --rk-stories-*
Estado en la URL
Enlaces compartibles ?story=grupo.story
Estado de visto
Los anillos vistos y la reanudación sobreviven a una recarga

Instalación

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

Registra la hoja de estilos una sola 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"]
Iconos

La cabecera y las flechas de navegación por defecto usan lucide-angular para los iconos. Si prefieres otra librería de iconos, sustitúyelas con los slots de plantilla rkStoriesHeader y rkStoriesNavigation.

Inicio rápido

rk-stories-overlay renderiza el player a pantalla completa. Combínalo con rk-stories-ring-list para tener los puntos de entrada al estilo de Instagram: un anillo emite (selected) con el índice de su grupo, que el host guarda como initialGroupIndex antes de activar isOpen. El player nunca se cierra por sí solo; emite (closed) y el host vuelve a poner isOpen a 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);
  }
}
Dónde se renderiza el overlay

A diferencia de los reproductores de React y Vue, que mueven su overlay al final de document.body, rk-stories-overlay se renderiza donde lo colocas y cubre la pantalla con position: fixed. Mantenlo fuera de cualquier ancestro con transform, perspective, filter o will-change: transform: ese ancestro pasa a ser el bloque contenedor del overlay, así que el reproductor toma su tamaño en lugar del del viewport y queda recortado por su overflow. Un ancestro que crea su propio contexto de apilamiento también limita cuánto puede elevar --rk-stories-overlay-z al reproductor. A cambio, los tokens --rk-stories-* definidos en cualquier ancestro llegan al reproductor.

Estado en la URL

Ver demo en vivo →

RkStoriesUrlOverlayComponent es un componente aparte cuyo estado de apertura vive en la barra de direcciones. Los dos ejes viajan en un solo parámetro — ?story=<group>.<story> —, así que la story en reproducción tiene un enlace que se puede compartir, guardar en marcadores y cerrar con el botón atrás. Crea un controlador con createOverlayUrlState y urlIndexTwoAxisKey, y pásalo como controller.

Claves incluidas

Las stories tienen dos ejes, así que despliega una clave de dos ejes en el controlador: urlIndexTwoAxisKey (autor y story por posición) o urlStableIdTwoAxisKey (el autor por un id estable); las dos se reexportan desde @reelkit/angular-stories-player. Consulta la guía de estado en la URL y la 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 añade una entrada al historial. Moverse por las stories y cambiar de autor solo la sustituyen, así que N navegaciones no añaden entradas y un paso atrás siempre cierra el player. Atrás cierra; no retrocede una story.
  • La navegación interior también viaja en la URL. Avanzar dentro de las stories de un autor actualiza ?story=2.n, así que un enlace directo aterriza en la story exacta.
  • Atrás solo cierra cuando el player se abrió desde dentro de la aplicación, porque ese enlace añadió una entrada. Un enlace compartido y abierto en una pestaña nueva no tiene historial detrás, así que el atrás del navegador sale del sitio; el botón ✕ o Escape quitan el parámetro en el sitio y se quedan.
  • Un parámetro que no nombra ni autor ni story —un marcador viejo, un valor editado a mano, una story más allá del final del grupo— se elimina de la URL en lugar de abrir la vecina.
  • Una aplicación con enrutador debería pasar un adaptador basado en él, para que el enrutador siga siendo la única fuente de verdad de la navegación. createRouterUrlAdapter, de @reelkit/angular/ng-router-url-adapter, es el adaptador listo para el enrutador de Angular; escribir el historial por detrás del enrutador deja su location desactualizada y tira el parámetro en la siguiente navegación.

Enlaces estables. El autor se direcciona por posición de forma predeterminada, así que un ?story=2.0 guardado abre a otra persona en cuanto se reordena el feed. Direcciónalo por un id estable: outerCodec escribe el id en la URL y outerLocator encuentra dónde está. Pasa innerCodec junto con innerLocate e innerIdentify para hacer lo mismo con la story dentro de su grupo; si los omites, la mitad de la story sigue siendo un índice local normal.

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 es cosa de outerLocator, con independencia del códec. locate es síncrono, así que solo responde por los grupos ya cargados: un enlace compartido al grupo 400 de un feed que ha cargado 20 se queda sin respuesta. locateAsync es el recurso siguiente, y solo se llama cuando locate falla; la story se vuelve a acotar al grupo en el que todo acaba deteniéndose.

El mismo locateAsync, eje exterior

Es el mismo paginador locateAsync que aceptan las claves de un eje: en una clave de dos ejes viaja en el outerLocator que le pasas, así que la paginación ocurre en el eje de los autores mientras la story sigue siendo un índice local dentro del grupo resuelto.

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;
    },
  },
});
  • Mientras locateAsync está pendiente, el player sigue cerrado y el parámetro intacto, así que el enlace directo sobrevive a la petición. Un null o un rechazo eliminan el parámetro.
  • Una respuesta que llega después de que la URL haya cambiado, después de cerrar o después de desmontar se descarta: una petición lenta no puede abrir una story que ya nadie pidió.
  • Todas las opciones de createOverlayUrlState están en la referencia de la API de Angular, y el recorrido paso a paso está en la guía de Angular.

Recordar lo que ya se vio

Los anillos muestran un degradado hasta que un grupo se ha visto hasta el final, y un grupo se reabre en la primera story que el espectador no ha visto. Las dos cosas salen de un único createStoriesViewedStateController, creado una vez donde vive el feed y pasado a la lista de anillos y al player como viewed: ellos leen el almacén, lo siguen y registran por sí mismos lo que se muestra. Una entrada guardada nombra al autor y a la story por su id, así que sobrevive a que el feed se reordene, y el mismo controlador funciona en React y en 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);
  }
}
  • La story inicial cuenta. La story que ya está en pantalla se marca como vista al abrirse el player, así que abrir un grupo de una sola story y cerrarlo lo marca como visto.
  • Deslizar hacia delante también reanuda. Se consulta resumeStoryIndex para cada grupo al que se llega por primera vez en la sesión, incluido aquel en el que se abre el player, salvo que initialStoryIndex nombre una story de forma explícita; un grupo por el que ya se ha pasado se reabre donde se dejó. Un input resumeStoryIndex explícito tiene prioridad sobre la respuesta del controlador.
  • Un enlace sigue mandando. Con rk-stories-url-overlay, el parámetro decide dónde se abre el player, sea lo que sea lo guardado. En cualquier otro caso decide el callback de reanudación.
  • Un recuento es una posición, no una suma. Una entrada nombra la story más lejana a la que se llegó, así que añadir una story a un grupo ya visto vuelve a encender su anillo, y quitar una del medio acorta el recuento.
  • El almacenamiento es intercambiable. Pasa storage: createSessionStorageAdapter() para olvidarlo al cerrar la pestaña, o tu propio StorageAdapter. Dos pestañas abiertas se mantienen sincronizadas mediante el evento storage del navegador.
  • Un player que solo se crea al abrirse lee demasiado tarde. rk-stories-overlay lee el almacén en cuanto existe, aunque siga cerrado, y lo mismo hace rk-stories-ring-list. Envuelto en @if (open), el overlay no tiene ese momento: llama a attachViewedState(() => this.viewed) en el constructor del host, que lo conecta mientras el host viva. Las opciones, y las piezas de más bajo nivel sobre las que se construye, están en la página de Stories Core.

Define desktopLayout="carousel" para distribuir el player como el visor web de Instagram: la story activa se queda en el centro con su tamaño habitual, y hasta dos grupos vecinos se colocan a cada lado como tarjetas más pequeñas y oscurecidas. Al hacer clic en una tarjeta se abre ese grupo, y las tarjetas se deslizan mientras cambia el grupo. Pasa el mismo controlador viewed que recibe la lista de anillos, y la tarjeta de un grupo ya visto lleva el anillo 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 {}
  • Los móviles mantienen el player simple. Hasta 768px de ancho las tarjetas no se renderizan y los deslizamientos usan groupTransition, como con el diseño 'single' por defecto. Redimensionar cruzando ese ancho cambia de uno a otro en vivo. Mientras se ven las tarjetas, los grupos se deslizan en lugar de girar, diga lo que diga groupTransition.
  • Cada tarjeta muestra dónde se abrirá el grupo: la story en la que se dejó durante esta sesión o, si no, lo que devuelva resumeStoryIndex. Muestra el póster del vídeo o la imagen. Una story sin ninguno de los dos, como una story de texto sobre un degradado, la dibuja tu plantilla rkStoriesSlide al tamaño del player y reducida; un vídeo sin póster muestra el fondo de la tarjeta. Esa vista previa recibe isActive: false y sus callbacks de ciclo de vida no hacen nada, así que no puede controlar el temporizador de la story en pantalla. Deja detrás de isActive todo lo que una slide haga al crearse, como la reproducción o la analítica.
  • El temporizador espera a la slide. La story no arranca su temporizador, y (storyViewed) no se emite para ella, hasta que las tarjetas terminan de moverse.
  • Toda forma de cambiar de grupo se desliza: un clic en una tarjeta, StoriesApi.goToGroup, nextGroup y prevGroup, y las flechas del teclado al pasar del final de un grupo. Un deslizamiento táctil mueve el propio player, así que no lleva un deslizamiento encima.
  • Cambiar el estilo de una tarjeta se hace con el slot rkStoriesGroupPreview, que se muestra en Tarjetas del carrusel personalizadas. El player sigue colocando, escalando y deslizando la tarjeta.

Barra de progreso y cabecera en cada grupo

Por defecto, una barra de progreso y una cabecera quedan encima del reproductor y pasan al nuevo grupo cuando ya ha cambiado. Define chromePlacement="group" para que cada grupo tenga las suyas dentro de su diapositiva, y las dos girarán con el grupo, como en 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 {}
  • Un grupo vecino muestra dónde se quedó. Mientras el reproductor gira hacia él, su barra y su cabecera muestran la story en la que se abrirá, sin nada reproducido aún.
  • El grupo que se abandona se queda como estaba. Su barra conserva el progreso hasta que termina el giro, así que un grupo visto hasta el final sigue lleno en lugar de vaciarse a mitad del giro.
  • Las plantillas se renderizan para cada grupo en pantalla. Los contextos de rkStoriesProgressBar y rkStoriesHeader incluyen groupIndex e isActive; para un grupo vecino isActive es false y las señales de progreso no cambian.
  • Una plantilla rkStoriesHeader está en la zona de deslizamiento. Un toque en ella cambia de story salvo que caiga en un button, un enlace o un elemento con role="button", así que haz que cada control sea uno de ellos.
  • El carrusel en escritorio no cambia durante el deslizamiento. El reproductor está oculto mientras las tarjetas se deslizan, así que la opción se ve cuando el grupo ya está abierto.

Slots de plantilla

Ocho directivas ng-template sustituyen cualquier parte del player. Cada una recibe un contexto tipado en el que todo valor tiene nombre, los mismos que usan los slots de React y Vue, así que let-author="author" toma el autor en el slot de cabecera. El valor principal también es el implícito, así que let-author solo hace lo mismo. Un slot que no proporciones mantiene la presentación por defecto.

DirectivaClaseContextoDescripción
rkStoriesErrorRkStoriesErrorDirective{ $implicit: story, story, storyIndex, groupIndex }Sustituye el panel "Content unavailable" integrado, dibujado sobre la story activa
rkStoriesFooterRkStoriesFooterDirective{ $implicit: story, story, author, storyIndex }Añade un pie bajo la story activa; el player no dibuja ninguno propio
rkStoriesGroupPreviewRkStoriesGroupPreviewDirective{ $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen }Rellena una tarjeta del carrusel de escritorio; el player sigue colocándola, escalándola y deslizándola
rkStoriesHeaderRkStoriesHeaderDirective{ $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose }Sustituye la cabecera por defecto (autor, pausa, sonido, cerrar). Con chromePlacement="group" está en la zona de deslizamiento; consulta Barra de progreso y cabecera en cada grupo.
rkStoriesLoadingRkStoriesLoadingDirective{ $implicit: story, story, storyIndex, groupIndex }Sustituye el indicador de carga de la cabecera mientras la story activa carga
rkStoriesNavigationRkStoriesNavigationDirective{ onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } }Sustituye ambas flechas de escritorio
rkStoriesProgressBarRkStoriesProgressBarDirective{ $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive }Sustituye la barra de progreso en canvas. activeIndex y progress son señales del core: conéctalas con toAngularSignal.
rkStoriesSlideRkStoriesSlideDirective{ $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }Sustituye las slides de imagen y vídeo por defecto
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 las ocho clases como un único array, para un componente que importe varias.

Cada slot se renderiza con el inyector del propio player, así que un componente dibujado dentro de uno resuelve los proveedores del player: un rk-video-story-slide en una plantilla de slide encuentra el mismo estado de sonido que escribe el botón de la cabecera.

Slides personalizadas

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 {}

Una slide personalizada le cuenta al player cómo va su contenido mediante callbacks del contexto de rkStoriesSlide. El player solo actúa sobre ellos para la story en pantalla; una llamada desde cualquier otra slide se ignora.

CallbackCuándo
onReadyEl contenido está listo (imagen cargada, vídeo reproduciéndose). El temporizador de progreso arranca, o espera a que termine el deslizamiento del carrusel.
onWaitingEl contenido se detiene (el vídeo se queda cargando a mitad de reproducción). Aparece el indicador de carga y el temporizador se pausa.
onErrorEl contenido no se pudo cargar. Se muestra el panel de error y el temporizador se pausa.
onDurationReadyInforma de la duración real del contenido en milisegundos, por ejemplo a partir de los metadatos del vídeo, para que el temporizador se ajuste a ella. Una duration definida en la story tiene prioridad.
onEndedEl contenido terminó por sí solo, por ejemplo un vídeo que llegó al final. El player pasa a la siguiente story sin esperar al temporizador.

rk-image-story-slide emite (loaded) y (failed), y rk-video-story-slide emite (playbackStarted), (buffering), (failed), (durationReady) y (finished), así que una plantilla de slide que los envuelva solo tiene que pasar cada uno al callback correspondiente.

Caché del precargador

Los componentes integrados RkImageStorySlideComponent y RkVideoStorySlideComponent precargan la siguiente story en segundo plano. Cuando el espectador llega a una story precargada, el contenido aparece al instante, sin indicador de carga. Una URL que falló una vez también se recuerda, así que reabrir una story rota muestra el error de inmediato en lugar de reintentar.

Navegación 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 progreso 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>

Carga y error 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>

Transiciones

groupTransition controla el efecto 3D al cambiar de autor. Importa las funciones de transición desde @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;
}

Doble toque y "me gusta"

Al tocar dos veces se reproduce una animación de corazón, como respuesta visual inmediata. (doubleTapped) emite el índice de grupo y de story para que guardes el "me gusta" en tu propio estado. El player no guarda ningún estado de "me gusta".

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 la animación del corazón

Ajusta la velocidad con el token --rk-stories-heart-duration (ver Temas). Para el color, el tamaño o para ocultar el corazón por completo, apunta a la clase .rk-stories-heart. RkHeartAnimationComponent se exporta para usarlo por 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;
}

La animación del corazón integrada todavía no puede sustituirse con un slot. Cámbiale el estilo con CSS, u ocúltala con display: none y lanza tu propia animación desde (doubleTapped). Si necesitas un slot para el corazón, dínoslo en GitHub Issues.

StoriesApi

Toma la API desde (apiReady) para controlar el player de forma imperativa. El player se reconstruye en cada apertura, así que un identificador guardado de una apertura anterior queda obsoleto: usa el más reciente.

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étodoTipoDescripción
nextStory()() => voidAvanza a la siguiente story dentro del grupo actual
prevStory()() => voidVuelve a la story anterior dentro del grupo actual
nextGroup()() => voidCambia al siguiente grupo de autor
prevGroup()() => voidCambia al grupo de autor anterior
goToGroup(index)(index: number) => voidSalta a un grupo concreto por índice
pause()() => voidPausa el avance automático y el temporizador de progreso
resume()() => voidReanuda el avance automático y el temporizador de progreso

Subcomponentes

Piezas exportadas, para componer dentro de los slots o usarlas por separado.

ComponenteQué es
RkCanvasProgressBarComponentLa barra de progreso segmentada en canvas, dibujada directamente desde las señales del temporizador. Gestiona una ventana deslizante para grupos con muchas stories. Con [live]="false" solo se dibuja cuando cambian sus señales y tras un cambio de tamaño, sin bucle de animación. Los tamaños y colores son inputs separados: gap, barHeight, minSegmentWidth, bgColor, fillColor.
RkHeartAnimationComponentEl corazón del doble toque. Emite (completed) cuando termina la animación, para que el padre lo retire.
RkImageStorySlideComponentSlide de imagen a sangre completa con object-fit: cover, que informa de la carga y del error mediante outputs.
RkStoriesRingComponentAvatar circular con el anillo degradado. Dos estados: si queda algo por ver rota, y si está todo visto queda plano. Emite (clicked).
RkStoriesRingListComponentFila horizontal de anillos con los nombres de los autores, uno por grupo. Emite (selected) con el índice del grupo.
RkStoryHeaderComponentLa cabecera por defecto: avatar, nombre, insignia de verificado, tiempo relativo, pausa, sonido, cerrar y el indicador de carga. Los botones de pausa y sonido solo aparecen con [showPauseButton] y [showSoundButton] activados, como hace el player en su propia cabecera.
RkVideoStorySlideComponentSlide de vídeo respaldada por el único elemento <video> compartido, para la continuidad del sonido en iOS. Gestiona la reproducción automática, los pósteres y el aviso de duración.

RkStoriesCarouselComponent también se exporta, junto con su tipo CarouselSlide, pero el overlay es dueño de todos los valores que dibuja: usa el slot rkStoriesGroupPreview para dar estilo a una tarjeta en lugar de montar el carrusel por tu cuenta.

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

Extiende StoryItem con tus propios campos y tipa los grupos con él:

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' },
    ],
  },
];

Reexportaciones

Todo lo que necesita un consumidor sale de este paquete, así que nunca hace falta importar directamente de @reelkit/core.

DesdeReexportado
@reelkit/angularSoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, y los tipos OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter
@reelkit/stories-corecreateStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, y los tipos de contenido StoryItem, AuthorInfo, StoriesGroup, MediaType

attachViewedState también se exporta, para un componente que sea dueño de un controlador de estado de visto y quiera mantenerlo activo mientras ese componente exista.

Inputs de RkStoriesOverlayComponent

rk-stories-overlay

InputTipoPor defectoDescripción
ariaLabelstring'Stories player'Nombre accesible de la región de diálogo; los lectores de pantalla lo anuncian al abrirse el overlay
chromePlacement'overlay' | 'group''overlay'Dónde viven la barra de progreso y la cabecera. 'overlay' dibuja una copia encima del reproductor; 'group' dibuja una en cada diapositiva de grupo, y las dos giran con el grupo. El tipo se exporta como ChromePlacement.
defaultImageDurationnumber5000Duración del avance automático de una story de imagen, en milisegundos
desktopLayout'single' | 'carousel''single'Disposición en escritorio. 'carousel' muestra los grupos vecinos como tarjetas de vista previa junto a la story activa y desliza entre grupos. Los móviles siempre muestran la story sola. El tipo se exporta como DesktopLayout.
enableKeyboardbooleantrueActiva la navegación por teclado (flechas izquierda/derecha, Escape)
groupsStoriesGroup<T>[]requiredGrupos de stories a mostrar. Los grupos o stories añadidos con el player abierto se recogen sin perder el punto en el que iba el espectador.
groupTransitionTransitionTransformFncubeTransitionTransición entre autores. Se ignora mientras se muestra el carrusel de escritorio, que desliza en su lugar.
hideUIOnPausebooleantrueOculta la barra de progreso y la cabecera mientras una pulsación larga mantiene la pausa
initialGroupIndexnumber0Índice (desde cero) del grupo con el que abre el player
initialStoryIndexnumberreanudar, si no 0Índice (desde cero) de la story con la que abre el player. Indicar una gana sobre cualquier cosa recordada; si lo omites, el grupo de apertura se reanuda como cualquier otro.
innerTransitionDurationnumber200Duración del fundido entre stories de un mismo autor, en milisegundos
isOpenbooleanrequiredRenderiza el player y bloquea el desplazamiento del body mientras es true
minSegmentWidthnumber8Anchura mínima de un segmento de progreso antes de que la barra pase a desplazar una ventana de segmentos, en píxeles
resumeStoryIndex(groupIndex: number) => number—Con qué story abre un grupo la primera vez que se llega a él. Solo se consulta para un grupo aún no visitado en esta apertura, y gana sobre viewed.
tapZoneSplitnumber0.3Proporción de la zona de toque (0–1). La parte izquierda retrocede, el resto avanza.
viewedStoriesViewedStateController—Lo que el espectador ya ha visto, de createStoriesViewedStateController(). Los grupos se reanudan en su primera story sin ver, se registra cada story mostrada y las tarjetas del carrusel apagan el anillo de un grupo visto hasta el final. Pasa el mismo controlador a rk-stories-ring-list.

Cada slot tiene además un input, para un componente que guarde el TemplateRef en lugar de proyectarlo: slideTemplate, headerTemplate, footerTemplate, progressBarTemplate, navigationTemplate, loadingTemplate, errorTemplate y groupPreviewTemplate. Una plantilla indicada así gana sobre otra proyectada dentro del player.

Outputs de RkStoriesOverlayComponent

OutputCargaDescripción
(apiReady)StoriesApiSe emite cuando el player está listo para recibir órdenes, y de nuevo en cada apertura
(closed)voidSe emite cuando el espectador cierra el player. El player no se mueve; decide el anfitrión. En rk-stories-url-overlay el parámetro ya está limpio para entonces.
(doubleTapped){ groupIndex, storyIndex }Se emite al tocar dos veces, junto con la animación del corazón
(groupChanged)numberSe emite después de cambiar el grupo activo
(paused)voidSe emite con una pulsación larga, o con el botón de pausa de la cabecera
(resumed)voidSe emite al soltar, o con el botón de reproducción de la cabecera
(storyChanged){ groupIndex, storyIndex }Se emite después de cambiar la story activa
(storyCompleted){ groupIndex, storyIndex }Se emite cuando se agota el temporizador de una story
(storyViewed){ groupIndex, storyIndex }Se emite cuando una story aparece en pantalla, incluida la de apertura

El player informa del cierre en lugar de cerrarse solo, así que (closed) cubre todas las salidas: el botón ✕, un deslizamiento hacia abajo, Escape y el final de la última story del último grupo.

Inputs de RkStoriesUrlOverlayComponent

rk-stories-url-overlay

Acepta todos los inputs anteriores salvo el trío de estado de apertura — isOpen, initialGroupIndex, initialStoryIndex — que vienen del controlador.

InputTipoPor defectoDescripción
controllerUrlStateController<TwoAxisPosition>requiredControlador de createOverlayUrlState combinado con urlIndexTwoAxisKey. Su posición — un objeto { outer, inner } — decide si el player está abierto y por dónde abre; el overlay la reescribe en cada navegación y al cerrar.

Interfaces StoryItem, AuthorInfo y 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 de los slots

Todos los tipos de contexto se exportan, para anotar un slot donde el compilador no pueda inferirlo: StoriesSlideContext, StoriesHeaderContext, StoriesFooterContext, StoriesProgressBarContext, StoriesNavigationContext, StoriesLoadingContext, StoriesErrorContext y StoriesGroupPreviewContext, junto con StoriesNavigationActions, DesktopLayout, ChromePlacement y 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';

Clases CSS

Los nombres de clase son planos (sin ámbito), así que una hoja de estilos cargada después de @reelkit/angular-stories-player/styles.css puede sobrescribir cualquiera. Son idénticos a los de @reelkit/react-stories-player y @reelkit/vue-stories-player, así que una hoja escrita para un binding sirve para los otros. Para colores, tamaños y z-index es mejor usar las propiedades personalizadas de Temas.

ClaseComponenteDescripción
.rk-stories-overlayOverlayFondo fijo a pantalla completa (color, z-index)
.rk-stories-overlay--carouselOverlayOverlay con el carrusel de escritorio visible
.rk-stories-overlay--slidingOverlayDeslizamiento del carrusel en curso; el lienzo del player está oculto
.rk-stories-swipe-wrapperOverlayContenedor del deslizamiento para cerrar (aloja las flechas y el lienzo)
.rk-stories-containerOverlayLienzo redondeado de la story (posición, overflow)
.rk-stories-ui-layerOverlayUna capa de interfaz sobre el player (cabecera, progreso, corazones). Con chromePlacement="group", una dentro de cada diapositiva de grupo (cabecera, progreso), y los corazones quedan directamente en el player
.rk-stories-ui-layer--hiddenOverlayInterfaz oculta (la alterna hideUIOnPause)
.rk-stories-errorOverlayEstado de error (icono y texto centrados)
.rk-stories-error-textOverlayTexto del mensaje de error
.rk-stories-nav-btnOverlayFlecha de anterior/siguiente en escritorio
.rk-stories-progress-barProgressBarContenedor que posiciona la barra de progreso en canvas
.rk-stories-slide-wrapperOverlayUn grupo de stories (slide exterior)
.rk-stories-storyOverlayUna sola story (raíz de la slide interior)
.rk-stories-imageImageStorySlideImagen a sangre completa (object-fit: cover)
.rk-stories-videoVideoStorySlideContenedor de la slide de vídeo
.rk-stories-video-elementVideoStorySlideEl elemento <video> compartido
.rk-stories-video-posterVideoStorySlidePóster, visible hasta que arranca la reproducción
.rk-stories-video-poster--visibleVideoStorySlideEstado visible del póster (antes de reproducir)
.rk-stories-heartHeartAnimationEl corazón del doble toque
.rk-stories-headerStoryHeaderFila de cabecera (avatar, nombre, tiempo, controles)
.rk-stories-header--hiddenStoryHeaderEstado oculto de la cabecera (input visible a false)
.rk-stories-header-avatarStoryHeaderImagen del avatar del autor
.rk-stories-header-nameStoryHeaderTexto con el nombre del autor
.rk-stories-header-verifiedStoryHeaderContenedor de la insignia de verificado
.rk-stories-header-timeStoryHeaderTexto con el tiempo transcurrido
.rk-stories-header-actionsStoryHeaderControles del lado derecho (sonido, pausa, cerrar)
.rk-stories-header-btnStoryHeaderBotones de pausa, sonido y cerrar
.rk-stories-header-btn--desktopStoryHeaderEl botón de pausa, visible solo con más de 768px de ancho
.rk-stories-header-spinnerStoryHeaderIndicador de carga mientras la story activa carga
.rk-stories-ringStoriesRingAvatar circular con el anillo degradado
.rk-stories-ring--activeStoriesRingAnillo con stories por ver (degradado en rotación)
.rk-stories-ring-avatarStoriesRingLa imagen del avatar dentro del anillo
.rk-stories-ring-listStoriesRingListFila horizontal de anillos
.rk-stories-ring-list-itemStoriesRingListColumna con anillo y nombre
.rk-stories-ring-list-nameStoriesRingListNombre del autor debajo de cada anillo
.rk-stories-carouselCarouselCapa de tarjetas de vista previa detrás del player
.rk-stories-carousel--instantCarouselPrimer fotograma de un deslizamiento, colocado sin transición
.rk-stories-cardCarouselUna tarjeta de vista previa ya colocada
.rk-stories-card--centerCarouselTarjeta en la posición central durante un deslizamiento
.rk-stories-card--hiddenCarouselTarjeta más allá de las dos que se ven a cada lado
.rk-stories-card-buttonCarouselContenido por defecto de la tarjeta (el botón que abre el grupo)
.rk-stories-card-imageCarouselLa imagen de vista previa de la tarjeta (póster o imagen)
.rk-stories-card-frameCarouselUna plantilla de slide dibujada como vista previa de la tarjeta, a escala reducida
.rk-stories-card-scrimCarouselCapa que oscurece la vista previa
.rk-stories-card-infoCarouselColumna con el anillo, el nombre del autor y la hora
.rk-stories-card-nameCarouselNombre del autor
.rk-stories-card-timeCarouselTexto con el tiempo transcurrido

Temas

Cada color, tamaño, duración y z-index es una propiedad personalizada --rk-stories-* en :root, así que un cambio de marca es una hoja de estilos y no un cambio de componente. Los tokens se comparten con los players de React y Vue.

TokenPor defectoControla
--rk-stories-overlay-bg#000Color del fondo a pantalla completa
--rk-stories-overlay-z9999z-index del overlay
--rk-stories-swipe-gap16pxEspacio entre los botones de navegación y el lienzo de la story
--rk-stories-container-radius12pxEsquinas redondeadas del lienzo de la story (escritorio)
--rk-stories-container-radius-mobile0Esquinas del lienzo de la story en un móvil
--rk-stories-ui-z15z-index de la capa de interfaz (cabecera, barra de progreso)
--rk-stories-ui-transition200msDuración del fundido cuando se alterna hideUIOnPause
--rk-stories-top-shade-height120pxAltura del degradado superior detrás de la cabecera
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Color del degradado superior
--rk-stories-error-z5z-index del panel de error
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Degradado del fondo del estado de error
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Color del icono y el texto de error
--rk-stories-error-gap12pxEspacio entre el icono de error y el texto
--rk-stories-error-text-size13pxTamaño de letra del mensaje de error
--rk-stories-nav-z20z-index de los botones de navegación en escritorio
--rk-stories-nav-size44pxTamaño de los botones de anterior y siguiente en escritorio
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Fondo de los botones de navegación en escritorio
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Fondo de los botones de navegación en escritorio al pasar el ratón
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Color del icono de los botones de navegación en escritorio
--rk-stories-nav-fg-hover#fffColor del icono de los botones de navegación en escritorio al pasar el ratón
--rk-stories-nav-transition150msTransición de los botones de navegación en escritorio al pasar el ratón
--rk-stories-video-bg#000Fondo de las bandas detrás del <video>
--rk-stories-video-poster-transition200msDuración del fundido del póster cuando el vídeo empieza a reproducirse
--rk-stories-progress-bar-z10z-index de la barra de progreso
--rk-stories-progress-bar-padding8px 8px 0Espacio alrededor de la barra de progreso
--rk-stories-card-bg#262626Fondo de la tarjeta del carrusel, visible cuando no hay imagen de vista previa
--rk-stories-card-radius8pxRadio de las esquinas de la tarjeta del carrusel
--rk-stories-card-scrimrgba(0, 0, 0, 0.45)Oscurecimiento sobre una tarjeta lateral
--rk-stories-card-fg#fffColor del texto de la tarjeta
--rk-stories-card-name-size14pxTamaño de letra del nombre del autor en la tarjeta
--rk-stories-card-time-fgrgba(255, 255, 255, 0.7)Color del tiempo transcurrido en la tarjeta
--rk-stories-card-time-size13pxTamaño de letra del tiempo transcurrido en la tarjeta
--rk-stories-card-gap6pxEspacio entre el anillo, el nombre y la hora en la tarjeta
--rk-stories-card-transition300msDuración del deslizamiento del carrusel
--rk-stories-ring-spin-duration4sDuración del giro de un anillo con stories por ver
--rk-stories-ring-active-scale0.95Escala del anillo mientras se pulsa
--rk-stories-ring-active-gradientconic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433)Lo que muestra un anillo con stories por ver: cualquier imagen o color CSS. En una página clara, defínelo en .rk-stories-ring-list y no en :root, para que el carrusel del reproductor conserve el suyo
--rk-stories-ring-viewed-colorrgba(255, 255, 255, 0.25)Lo que muestra un anillo visto entero, pensado para una página oscura. En una clara, defínelo en .rk-stories-ring-list y no en :root
--rk-stories-ring-gradientnoneInterno: el anillo lo escribe en cada render y apunta a los dos tokens de arriba, salvo que el anillo reciba gradientColors o viewedColor
--rk-stories-ring-list-gap12pxEspacio entre los anillos de la lista
--rk-stories-ring-list-padding12pxRelleno interior alrededor de la lista de anillos
--rk-stories-ring-list-item-gap4pxEspacio entre un anillo y su nombre
--rk-stories-ring-list-name-size12pxTamaño de letra del nombre del autor debajo de cada anillo
--rk-stories-header-z5z-index de la cabecera
--rk-stories-header-top18pxSeparación vertical de la cabecera desde la parte superior de la story
--rk-stories-header-padding12px 16pxRelleno interior de la fila de la cabecera
--rk-stories-header-gap8pxEspacio entre los elementos de la fila de la cabecera
--rk-stories-header-transition200msDuración del fundido de la cabecera cuando se oculta
--rk-stories-header-avatar-size32pxAncho y alto del avatar
--rk-stories-header-name-fg#fffColor del nombre del autor
--rk-stories-header-name-size14pxTamaño de letra del nombre del autor
--rk-stories-header-name-weight600Grosor de letra del nombre del autor
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Color del texto con el tiempo transcurrido
--rk-stories-header-time-size12pxTamaño de letra del tiempo transcurrido
--rk-stories-header-actions-gap8pxEspacio entre los botones de la cabecera
--rk-stories-header-btn-fg#fffColor de los iconos de acción de la cabecera (cerrar, sonido, pausa)
--rk-stories-header-btn-padding4pxRelleno de los botones de la cabecera
--rk-stories-header-spinner-size20pxTamaño del indicador de carga
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)Color de la pista del indicador de carga
--rk-stories-header-spinner-fg#fffColor del arco del indicador de carga
--rk-stories-header-spinner-duration0.8sUna vuelta del indicador de carga
--rk-stories-heart-z20z-index del corazón del doble toque
--rk-stories-heart-duration800msDuración de la animación de aparición y desvanecimiento

Pega el fragmento de abajo en una hoja de estilos cargada después 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;
}

Accesibilidad

La raíz del overlay es un diálogo modal (role="dialog", aria-modal="true"). Usa ariaLabel para cambiar lo que anuncia el lector de pantalla; por defecto es "Stories player".

El overlay captura el foco al abrirse y lo devuelve al disparador al cerrarse. Tab y Shift+Tab recorren los elementos enfocables de dentro, dando la vuelta en ambos extremos; el foco que se escapa (clic fuera, foco programático) vuelve a entrar. El desplazamiento del body queda bloqueado mientras el player está abierto. Implementado con captureFocusForReturn y createFocusTrap de @reelkit/core, reexportados por @reelkit/angular.

Con el carrusel de escritorio, cada tarjeta por defecto es un botón etiquetado "Open stories by" y el nombre del autor. Las tarjetas van después de los controles del player en el orden de tabulación, y salen de él mientras se ejecuta un deslizamiento; una tarjeta que abre su grupo devuelve el foco al diálogo en lugar de dejarlo caer en la página.

Renderizado en servidor

El player no renderiza nada hasta que está abierto y no lee ninguna API del navegador durante la construcción, así que es seguro con Angular SSR. El estado de visto se lee después del montaje y no durante el renderizado, y eso es lo que mantiene de acuerdo al render del servidor y al primer render del cliente.

Atajos de teclado

TeclaAcción
ArrowLeftStory anterior. En la primera story de un grupo, el grupo anterior, abierto donde se dejó; en el primer grupo no hace nada.
ArrowRightStory siguiente. En la última story de un grupo, el grupo siguiente; en el último grupo, el player se cierra.
EscapeCerrar el player