Angular Stories Player

Ein Stories Player im Instagram-Stil als Overlay für Angular, mit @reelkit/angular-stories-player. Er teilt die Engine @reelkit/stories-core mit den Playern für React und Vue, daher sind das Verhalten, die CSS-Klassen und die Theming-Tokens dieselben.

Live-Demo ansehen →

Funktionen

Verschachtelte Navigation
Tippen für die nächste Story, Wischen für die nächste Gruppe
Video-Stories
Autoplay mit Ton-Umschalter
Automatisches Weiterschalten
Einstellbarer Timer pro Story
3D-Übergänge
Cube, Flip, Fade, Zoom, Slide
Fortschrittsbalken
Segmentierter Fortschritt auf Canvas
Bild und Video
Verarbeitet beide Medientypen
Template-Slots
Acht Bereiche per ng-template
Like per Doppeltipp
Herz-Animation beim Doppeltippen
Desktop-Navigation
Pfeil-Buttons auf dem Desktop
Story-Ringe
Avatar-Ringe im Instagram-Stil
Generische Typen
StoryItem mit eigenen Daten erweitern
Theming-Tokens
Neues Branding allein mit --rk-stories-*
URL-State
Teilbare Links mit ?story=group.story
Gesehen-Status
Gesehene Ringe und Fortsetzen überstehen ein Neuladen

Installation

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

Registriere das Stylesheet einmal:

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"]
Symbole

Der Standard-Header und die Navigationspfeile nutzen lucide-angular für die Symbole. Wenn du eine andere Symbolbibliothek bevorzugst, ersetze sie über die Template-Slots rkStoriesHeader und rkStoriesNavigation.

Schnellstart

rk-stories-overlay rendert den Player im Vollbild. Kombiniere ihn mit rk-stories-ring-list für die Einstiegspunkte im Instagram-Stil: Ein Ring sendet (selected) mit seinem Gruppenindex, den der Host als initialGroupIndex übernimmt, bevor er isOpen setzt. Der Player schließt sich nie selbst; er sendet (closed), und der Host setzt isOpen zurück auf 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);
  }
}
Wo das Overlay gerendert wird

Anders als die Player für React und Vue, die ihr Overlay ans Ende von document.body verschieben, rendert rk-stories-overlay dort, wo du es platzierst, und bedeckt den Bildschirm mit position: fixed. Halte es von jedem Vorfahren mit transform, perspective, filter oder will-change: transform fern: Dieser Vorfahr wird zum Containing Block des Overlays, der Player richtet seine Größe dann nach ihm statt nach dem Viewport und wird von seinem overflow abgeschnitten. Ein Vorfahr mit eigenem Stacking Context begrenzt außerdem, wie weit --rk-stories-overlay-z den Player anheben kann. Im Gegenzug erreichen --rk-stories-*-Tokens, die auf einem beliebigen Vorfahren gesetzt sind, den Player.

URL-State

Live-Demo ansehen →

RkStoriesUrlOverlayComponent ist eine eigene Komponente, deren Öffnungszustand in der Adressleiste liegt. Beide Achsen stecken in einem Parameter – ?story=<group>.<story> –, sodass die laufende Story einen Link hat, den man teilen, als Lesezeichen speichern und mit der Zurück-Taste schließen kann. Baue einen Controller mit createOverlayUrlState und urlIndexTwoAxisKey und übergib ihn dann als controller.

Eingebaute Schlüssel

Stories haben zwei Achsen, daher verteilst du einen Schlüssel für zwei Achsen in den Controller: urlIndexTwoAxisKey (Gruppe und Story nach Position) oder urlStableIdTwoAxisKey (die Gruppe nach einer stabilen id) – beide werden von @reelkit/angular-stories-player re-exportiert. Siehe den Leitfaden zum URL-State und die 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`);
  }
}
  • Das Öffnen legt einen Eintrag im Verlauf an. Das Blättern durch Stories und der Wechsel zwischen Nutzern ersetzen ihn beide, sodass N Navigationen keine Einträge hinzufügen und ein Schritt zurück den Player immer schließt. Zurück schließt; es blättert nicht durch Stories.
  • Die innere Navigation wird mitgeführt. Das Weiterschalten innerhalb der Stories eines Nutzers aktualisiert ?story=2.n, sodass ein Deep Link genau auf der Story landet.
  • Zurück schließt nur, wenn der Player innerhalb der App geöffnet wurde – der Link hat einen Eintrag angelegt. Ein geteilter Link, der direkt in einem neuen Tab geöffnet wird, hat keinen Verlauf hinter sich, daher verlässt die Zurück-Taste des Browsers die Seite; der ✕-Button oder Escape entfernt den Parameter an Ort und Stelle und bleibt auf der Seite.
  • Ein Parameter, der keine Gruppe oder Story benennt – ein veraltetes Lesezeichen, ein von Hand geänderter Wert, eine Story hinter dem Ende einer Gruppe –, wird aus der URL entfernt, statt einen Nachbarn zu öffnen.
  • Eine App mit Routing sollte einen Adapter übergeben, der auf dem Router aufbaut, damit der Router die einzige Quelle für die Navigation bleibt. createRouterUrlAdapter aus @reelkit/angular/ng-router-url-adapter ist der fertige Adapter für den Angular-Router; wer den Verlauf an ihm vorbei schreibt, lässt dessen Location veralten und verliert den Parameter bei der nächsten Navigation.

Stabile Links. Die Gruppe ist standardmäßig positionsbasiert, daher öffnet ein gespeichertes ?story=2.0 einen anderen Nutzer, sobald der Feed umsortiert wird. Adressiere sie stattdessen über eine stabile id – outerCodec schreibt die id in die URL, outerLocator findet, wo sie steht. Übergib innerCodec mit innerLocate und innerIdentify, um dasselbe für die Story innerhalb ihrer Gruppe zu tun; lässt du sie weg, bleibt die Story-Hälfte ein einfacher lokaler Index.

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,
});

Endlose Feeds. Das Nachladen ist Sache des outerLocator und unabhängig vom Codec. locate ist synchron und antwortet daher nur für bereits geladene Gruppen – ein geteilter Link auf Gruppe 400 eines Feeds, der erst 20 geladen hat, findet nichts. locateAsync ist der Ausweg und wird nur aufgerufen, wenn locate nichts findet; die Story wird dann an der Gruppe neu begrenzt, bei der es landet.

Dasselbe locateAsync, äußere Achse

Das ist derselbe locateAsync-Pager, den auch die Schlüssel für eine Achse annehmen – bei einem Schlüssel für zwei Achsen hängt er am übergebenen outerLocator, sodass die Gruppenachse nachlädt, während die Story ein lokaler Index innerhalb der gefundenen Gruppe bleibt.

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;
    },
  },
});
  • Solange locateAsync aussteht, bleibt der Player geschlossen und der Parameter unangetastet, sodass der Deep Link das Laden übersteht. Ein null oder eine Ablehnung entfernt den Parameter.
  • Eine Antwort, die eintrifft, nachdem sich die URL geändert hat, nach einem Schließen oder nach dem Unmount, wird verworfen – ein langsames Laden kann keine Story öffnen, nach der niemand gefragt hat.
  • Alle Optionen von createOverlayUrlState findest du in der Angular-API-Referenz, die Schritt-für-Schritt-Anleitung im Angular-Leitfaden.

Merken, was gesehen wurde

Ringe zeigen einen Verlauf, bis eine Gruppe bis zum Ende angesehen wurde, und eine Gruppe öffnet sich wieder bei der ersten Story, die der Betrachter noch nicht gesehen hat. Beides kommt von einem einzigen createStoriesViewedStateController, der einmal dort erstellt wird, wo der Feed lebt, und der Ringliste und dem Player als viewed übergeben wird: Beide lesen den Speicher, folgen ihm und zeichnen selbst auf, was gezeigt wird. Ein gespeicherter Eintrag benennt Autor und Story per id, übersteht also ein Umsortieren des Feeds, und derselbe Controller funktioniert in React und 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);
  }
}
  • Die Story beim Öffnen zählt. Die Story, die schon auf dem Bildschirm ist, wird beim Öffnen des Players als gesehen gemeldet, sodass das Öffnen und Schließen einer Gruppe mit einer Story sie als angesehen markiert.
  • Auch Weiterwischen setzt fort. resumeStoryIndex wird für jede Gruppe befragt, die in dieser Sitzung zum ersten Mal erreicht wird, die Gruppe beim Öffnen eingeschlossen, außer initialStoryIndex nennt ausdrücklich eine Story; eine schon durchgewischte Gruppe öffnet sich dort, wo sie verlassen wurde. Ein ausdrücklicher Input resumeStoryIndex hat Vorrang vor der Antwort des Controllers.
  • Ein Link hat weiterhin Vorrang. Mit rk-stories-url-overlay entscheidet der Parameter, wo der Player öffnet, egal was gespeichert ist. Überall sonst entscheidet der Callback zum Fortsetzen.
  • Eine Zahl ist eine Position, keine Strichliste. Ein Eintrag nennt die am weitesten erreichte Story, daher leuchtet der Ring einer angesehenen Gruppe wieder auf, wenn eine Story hinzukommt, und das Entfernen einer Story aus der Mitte verkürzt die Zahl.
  • Der Speicher ist austauschbar. Übergib storage: createSessionStorageAdapter(), um beim Schließen des Tabs zu vergessen, oder deinen eigenen StorageAdapter. Zwei offene Tabs bleiben über das Storage-Event des Browsers synchron.
  • Ein Player, der erst beim Öffnen entsteht, liest zu spät. rk-stories-overlay liest den Speicher, sobald es existiert, auch solange es noch geschlossen ist, und rk-stories-ring-list ebenso. In @if (open) verpackt hat das Overlay diesen Moment nicht: Rufe attachViewedState(() => this.viewed) im Konstruktor des Hosts auf, das sich für die gesamte Lebensdauer des Hosts einhängt. Die Optionen und die tieferliegenden Bausteine, auf denen das aufbaut, findest du auf der Seite Stories Core.

Setze desktopLayout="carousel", um den Player wie den Web-Viewer von Instagram anzuordnen: Die aktive Story bleibt in normaler Größe in der Mitte, und bis zu zwei benachbarte Gruppen liegen auf jeder Seite als kleinere, abgedunkelte Karten. Ein Klick auf eine Karte öffnet diese Gruppe, und die Karten gleiten hinüber, während die Gruppe wechselt. Übergib denselben viewed-Controller, den auch die Ringliste erhält, dann bekommt die Karte einer angesehenen Gruppe den gedämpften Ring.

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 {}
  • Smartphones behalten den einfachen Player. Bis 768px Breite werden die Karten nicht gerendert, und Wischgesten nutzen groupTransition wie beim Standard-Layout 'single'. Eine Größenänderung über diese Breite hinweg schaltet live um. Solange die Karten sichtbar sind, gleiten Gruppen, statt sich zu drehen, egal was groupTransition sagt.
  • Jede Karte zeigt eine Vorschau der Stelle, an der die Gruppe öffnet: die Story, bei der sie in dieser Sitzung verlassen wurde, sonst das, was resumeStoryIndex zurückgibt. Sie zeigt das Posterbild des Videos oder das Bild. Eine Story ohne beides, etwa eine Text-Story auf einem Verlauf, wird von deinem rkStoriesSlide-Template in der Größe des Players gezeichnet und verkleinert; ein Video ohne Posterbild zeigt den Hintergrund der Karte. Diese Vorschau erhält isActive: false, und ihre Lifecycle-Callbacks tun nichts, sodass sie den Timer der Story auf dem Bildschirm nicht steuern kann. Halte alles, was ein Slide beim Erstellen tut, etwa Wiedergabe oder Analytics, hinter isActive.
  • Der Timer wartet auf den Slide. Die Story startet ihren Timer nicht, und (storyViewed) wird für sie nicht ausgelöst, bis die Karten ihre Bewegung beendet haben.
  • Jede Art des Gruppenwechsels gleitet: ein Klick auf eine Karte, StoriesApi.goToGroup, nextGroup und prevGroup und die Pfeiltasten über das Ende einer Gruppe hinaus. Eine Wischgeste per Touch bewegt den Player selbst und bekommt daher keinen zusätzlichen Slide.
  • Das Aussehen einer Karte ändern übernimmt der Slot rkStoriesGroupPreview, gezeigt unter Eigene Karussell-Karten. Der Player positioniert, skaliert und bewegt die Karte weiterhin.

Fortschrittsbalken und Header pro Gruppe

Standardmäßig sitzen ein Fortschrittsbalken und ein Header über dem Player und wechseln zur neuen Gruppe, sobald sie gewechselt hat. Setze chromePlacement="group", damit jede Gruppe eigene bekommt, die in ihrem Slide gezeichnet werden, sodass sich beide mit der Gruppe drehen, wie bei 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 {}
  • Eine benachbarte Gruppe zeigt ihren Stand. Während der Player zu ihr dreht, zeigen ihr Balken und ihr Header die Story, bei der sie öffnen wird, ohne dass davon schon etwas abgespielt ist.
  • Die verlassene Gruppe hält still. Ihr Balken behält seinen Fortschritt bis zum Ende der Drehung, sodass eine bis zum Ende abgespielte Gruppe voll bleibt, statt sich mitten in der Drehung zu leeren.
  • Die Templates werden für jede Gruppe auf dem Bildschirm gerendert. Die Kontexte von rkStoriesProgressBar und rkStoriesHeader tragen groupIndex und isActive; für eine benachbarte Gruppe ist isActive gleich false, und die Fortschritts-Signals halten still.
  • Ein rkStoriesHeader-Template liegt im Wischbereich. Ein Tippen darauf wechselt zwischen Stories, außer es trifft einen button, einen Link oder ein Element mit role="button" – mach also jedes Steuerelement darin zu einem davon.
  • Das Desktop-Karussell bleibt mitten im Slide unberührt. Der Player ist verborgen, solange die Karten gleiten, daher zeigt sich die Einstellung, sobald die Gruppe offen ist.

Template-Slots

Acht ng-template-Direktiven ersetzen jeden Teil des Players. Jede erhält einen typisierten Kontext, in dem jeder Wert benannt ist, mit denselben Namen wie in den Slots für React und Vue, sodass let-author="author" im Header-Slot den Autor bindet. Der Hauptwert ist zugleich der implizite, daher bewirkt let-author allein dasselbe. Ein Slot, den du weglässt, behält das eingebaute Rendering.

DirektiveKlasseKontextBeschreibung
rkStoriesErrorRkStoriesErrorDirective{ $implicit: story, story, storyIndex, groupIndex }Ersetzt das eingebaute Panel „Content unavailable“, das über der aktiven Story gezeichnet wird
rkStoriesFooterRkStoriesFooterDirective{ $implicit: story, story, author, storyIndex }Fügt unter der aktiven Story einen Footer hinzu; der Player zeichnet selbst keinen
rkStoriesGroupPreviewRkStoriesGroupPreviewDirective{ $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen }Füllt eine Karte des Desktop-Karussells; der Player positioniert, skaliert und bewegt sie weiterhin
rkStoriesHeaderRkStoriesHeaderDirective{ $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose }Ersetzt den Standard-Header (Autor, Pause, Ton, Schließen). Mit chromePlacement="group" liegt er im Wischbereich; siehe Fortschrittsbalken und Header pro Gruppe.
rkStoriesLoadingRkStoriesLoadingDirective{ $implicit: story, story, storyIndex, groupIndex }Ersetzt den Standard-Spinner im Header, während die aktive Story lädt
rkStoriesNavigationRkStoriesNavigationDirective{ onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } }Ersetzt beide Desktop-Pfeile
rkStoriesProgressBarRkStoriesProgressBarDirective{ $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive }Ersetzt den Canvas-Fortschrittsbalken. activeIndex und progress sind Core-Signals – verbinde sie mit toAngularSignal.
rkStoriesSlideRkStoriesSlideDirective{ $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }Ersetzt die Standard-Slides für Bild und Video
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 exportiert alle acht Klassen als ein Array, für eine Komponente, die mehrere davon importiert.

Jeder Slot rendert über den eigenen Injector des Players, sodass eine darin gezeichnete Komponente die Provider des Players auflöst – ein rk-video-story-slide in einem Slide-Template findet denselben Ton-Zustand, den der Button im Header schreibt.

Eigene Slides

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

Ein eigener Slide teilt dem Player über Callbacks im Kontext von rkStoriesSlide mit, wie es um seinen Inhalt steht. Der Player reagiert darauf nur für die Story auf dem Bildschirm; ein Aufruf von einem anderen Slide wird ignoriert.

CallbackWann
onReadyDer Inhalt ist bereit (Bild geladen, Video läuft). Der Fortschritts-Timer startet oder wartet, bis ein Karussell-Slide endet.
onWaitingDer Inhalt stockt (Video puffert mitten in der Wiedergabe). Der Spinner erscheint, und der Timer pausiert.
onErrorDer Inhalt konnte nicht geladen werden. Das Fehler-Panel wird angezeigt, und der Timer pausiert.
onDurationReadyMeldet die echte Medienlänge in Millisekunden, zum Beispiel aus den Video-Metadaten, damit der Timer zu ihr passt. Eine duration an der Story hat Vorrang.
onEndedDas Medium ist von selbst zu Ende, zum Beispiel ein bis zum Schluss abgespieltes Video. Der Player wechselt zur nächsten Story, ohne auf den Timer zu warten.

rk-image-story-slide sendet (loaded) und (failed), und rk-video-story-slide sendet (playbackStarted), (buffering), (failed), (durationReady) und (finished), sodass ein Slide-Template, das sie umschließt, jedes davon nur an den passenden Callback weiterreichen muss.

Zwischenspeicher des Vorladers

Die eingebauten RkImageStorySlideComponent und RkVideoStorySlideComponent laden die nächste Story im Hintergrund vor. Erreicht ein Betrachter eine vorgeladene Story, erscheint der Inhalt sofort, ohne Lade-Spinner. Auch eine URL, die einmal fehlgeschlagen ist, wird gemerkt, sodass das erneute Öffnen einer defekten Story den Fehler sofort zeigt, statt es noch einmal zu versuchen.

Eigene Navigation

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>

Eigener Fortschrittsbalken

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>

Eigenes Laden und eigener Fehler

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>

Übergänge

groupTransition steuert den 3D-Effekt beim Wechsel zwischen Nutzern. Importiere die Übergangsfunktionen aus @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;
}

Doppeltippen und Likes

Beim Doppeltippen spielt eine Herz-Animation und gibt sofort sichtbares Feedback. (doubleTapped) wird mit dem Index der Gruppe und der Story ausgelöst, damit du das Like in deinem eigenen Zustand speichern kannst. Der Player selbst hält keinen Like-Zustand.

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)"
/>

Die Herz-Animation anpassen

Die Geschwindigkeit stellst du mit dem Token --rk-stories-heart-duration ein (siehe Theming). Für Farbe, Größe oder das vollständige Ausblenden des Herzens sprichst du die Klasse .rk-stories-heart an. RkHeartAnimationComponent wird für die eigenständige Nutzung exportiert.

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

Die eingebaute Herz-Animation lässt sich noch nicht über einen Slot ersetzen. Gestalte sie mit CSS um, oder blende sie mit display: none aus und starte deine eigene Animation aus (doubleTapped). Wenn du einen Slot für das Herz brauchst, sag uns Bescheid über GitHub Issues.

StoriesApi

Nimm die API aus (apiReady) für die imperative Steuerung. Der Player wird bei jedem Öffnen neu aufgebaut, daher ist ein Handle aus einem früheren Öffnen veraltet – nutze immer das neueste.

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

Methoden

MethodeTypBeschreibung
nextStory()() => voidWechselt zur nächsten Story innerhalb der aktuellen Gruppe
prevStory()() => voidWechselt zur vorherigen Story innerhalb der aktuellen Gruppe
nextGroup()() => voidWechselt zur nächsten Nutzergruppe
prevGroup()() => voidWechselt zur vorherigen Nutzergruppe
goToGroup(index)(index: number) => voidSpringt per Index zu einer bestimmten Gruppe
pause()() => voidPausiert das automatische Weiterschalten und den Fortschritts-Timer
resume()() => voidSetzt das automatische Weiterschalten und den Fortschritts-Timer fort

Unterkomponenten

Exportierte Bausteine, um sie in den Slots zu kombinieren oder eigenständig zu nutzen.

KomponenteWas sie ist
RkCanvasProgressBarComponentDer segmentierte Fortschrittsbalken auf Canvas, direkt aus den Timer-Signals gezeichnet. Verarbeitet ein gleitendes Fenster für Gruppen mit vielen Stories. Mit [live]="false" zeichnet er nur, wenn sich seine Signals ändern, und nach einer Größenänderung, ohne Animationsschleife. Größen und Farben sind eigene Inputs: gap, barHeight, minSegmentWidth, bgColor, fillColor.
RkHeartAnimationComponentDas Herz beim Doppeltippen. Sendet (completed), wenn die Animation endet, damit das Elternelement es entfernen kann.
RkImageStorySlideComponentRandloser Bild-Slide mit object-fit: cover, der Laden und Fehler über Outputs meldet.
RkStoriesRingComponentRunder Avatar mit dem Verlaufsring. Zwei Zustände: Ist noch etwas anzusehen, dreht er sich, ist alles angesehen, wird er flach. Sendet (clicked).
RkStoriesRingListComponentHorizontale Reihe von Ringen mit Autorennamen, einer pro Gruppe. Sendet (selected) mit dem Gruppenindex.
RkStoryHeaderComponentDer Standard-Header: Avatar, Name, Verifizierungsabzeichen, relative Zeit, Pause, Ton, Schließen und der Lade-Spinner. Die Buttons für Pause und Ton erscheinen nur, wenn [showPauseButton] und [showSoundButton] gesetzt sind, so wie der Player sie für seinen eigenen Header setzt.
RkVideoStorySlideComponentVideo-Slide auf Basis des einen geteilten <video>-Elements, damit der Ton auf iOS durchgehend bleibt. Verarbeitet Autoplay, Posterbilder und die Meldung der Dauer.

RkStoriesCarouselComponent wird ebenfalls exportiert, mit seinem Typ CarouselSlide, aber das Overlay besitzt jeden Wert, aus dem es zeichnet – nutze den Slot rkStoriesGroupPreview, um eine Karte umzugestalten, statt das Karussell selbst einzubinden.

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

Eigene Story-Typen

Erweitere StoryItem um deine eigenen Felder und typisiere die Gruppen damit:

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

Re-Exporte

Alles, was ein Nutzer braucht, kommt aus diesem Paket, daher ist ein direkter Import von @reelkit/core nie nötig.

AusRe-exportiert
@reelkit/angularSoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter und die Typen OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter
@reelkit/stories-corecreateStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig und die Inhaltstypen StoryItem, AuthorInfo, StoriesGroup, MediaType

attachViewedState wird ebenfalls exportiert, für eine Komponente, die einen Controller für den Gesehen-Status besitzt und ihn für ihre gesamte Lebensdauer einhängen will.

Inputs von RkStoriesOverlayComponent

rk-stories-overlay

InputTypStandardBeschreibung
ariaLabelstring'Stories player'Barrierefreie Beschriftung für den Dialogbereich; wird von Screenreadern angesagt, wenn sich das Overlay öffnet
chromePlacement'overlay' | 'group''overlay'Wo Fortschrittsbalken und Header liegen. 'overlay' zeichnet eine Kopie über dem Player; 'group' zeichnet eine in jedem Gruppen-Slide, sodass sich beide mit der Gruppe drehen. Der Typ wird als ChromePlacement exportiert.
defaultImageDurationnumber5000Dauer des automatischen Weiterschaltens für eine Bild-Story, in Millisekunden
desktopLayout'single' | 'carousel''single'Layout auf dem Desktop. 'carousel' zeigt benachbarte Gruppen als Vorschaukarten neben der aktiven Story und gleitet zwischen den Gruppen. Smartphones zeigen die Story immer allein. Der Typ wird als DesktopLayout exportiert.
enableKeyboardbooleantrueAktiviert die Tastaturnavigation (Pfeil links/rechts, Escape)
groupsStoriesGroup<T>[]requiredDie anzuzeigenden Story-Gruppen. Gruppen oder Stories, die bei offenem Player hinzukommen, werden übernommen, ohne dass der Betrachter seine Stelle verliert.
groupTransitionTransitionTransformFncubeTransitionÜbergang zwischen Nutzern. Wird ignoriert, solange das Desktop-Karussell sichtbar ist, das stattdessen gleitet.
hideUIOnPausebooleantrueBlendet Fortschrittsbalken und Header aus, solange per langem Drücken pausiert ist
initialGroupIndexnumber0Nullbasierter Index der Gruppe, bei der der Player öffnet
initialStoryIndexnumberFortsetzen, sonst 0Nullbasierter Index der Story, bei der der Player öffnet. Eine genannte Story hat Vorrang vor allem Gemerkten; lässt du ihn weg, setzt die Gruppe beim Öffnen fort wie jede andere.
innerTransitionDurationnumber200Dauer der Überblendung zwischen Stories eines Nutzers, in Millisekunden
isOpenbooleanrequiredRendert den Player und sperrt das Scrollen der Seite, solange true
minSegmentWidthnumber8Die geringste Breite eines Fortschrittssegments, bevor der Balken stattdessen ein Fenster davon scrollt, in Pixeln
resumeStoryIndex(groupIndex: number) => number—Bei welcher Story eine Gruppe öffnet, wenn sie zum ersten Mal erreicht wird. Wird nur für eine Gruppe befragt, die während dieses Öffnens noch nicht besucht wurde, und hat Vorrang vor viewed.
tapZoneSplitnumber0.3Aufteilung der Tippzonen (0–1). Der linke Teil geht zurück, der Rest vorwärts.
viewedStoriesViewedStateController—Was der Betrachter gesehen hat, aus createStoriesViewedStateController(). Gruppen setzen bei ihrer ersten ungesehenen Story fort, jede gezeigte Story wird aufgezeichnet, und Karussell-Karten dämpfen den Ring einer bis zum Ende angesehenen Gruppe. Übergib denselben Controller an rk-stories-ring-list.

Jeder Slot hat auch einen Input, für eine Komponente, die die TemplateRef selbst hält, statt sie zu projizieren: slideTemplate, headerTemplate, footerTemplate, progressBarTemplate, navigationTemplate, loadingTemplate, errorTemplate und groupPreviewTemplate. Ein so benanntes Template hat Vorrang vor einem, das in den Player projiziert wird.

Outputs von RkStoriesOverlayComponent

OutputPayloadBeschreibung
(apiReady)StoriesApiWird ausgelöst, sobald der Player Befehle annehmen kann, und erneut bei jedem Öffnen
(closed)voidWird ausgelöst, wenn der Betrachter den Player schließt. Der Player bleibt, wo er ist; der Host entscheidet. Bei rk-stories-url-overlay ist der Parameter zu diesem Zeitpunkt schon entfernt.
(doubleTapped){ groupIndex, storyIndex }Wird bei einem Doppeltippen ausgelöst, zusammen mit der Herz-Animation
(groupChanged)numberWird ausgelöst, nachdem sich die aktive Gruppe geändert hat
(paused)voidWird bei einem langen Drücken oder über den Pause-Button im Header ausgelöst
(resumed)voidWird beim Loslassen oder über den Play-Button im Header ausgelöst
(storyChanged){ groupIndex, storyIndex }Wird ausgelöst, nachdem sich die aktive Story geändert hat
(storyCompleted){ groupIndex, storyIndex }Wird ausgelöst, wenn der Timer einer Story abläuft
(storyViewed){ groupIndex, storyIndex }Wird ausgelöst, wenn eine Story auf den Bildschirm kommt, die beim Öffnen eingeschlossen

Der Player meldet ein Schließen, statt sich selbst zu schließen, daher deckt (closed) jeden Ausweg ab: den ✕-Button, ein Wischen nach unten, Escape und das Ende der letzten Story der letzten Gruppe.

Inputs von RkStoriesUrlOverlayComponent

rk-stories-url-overlay

Nimmt jeden Input oben an außer dem Trio für den Öffnungszustand – isOpen, initialGroupIndex, initialStoryIndex –, das stattdessen vom Controller kommt.

InputTypStandardBeschreibung
controllerUrlStateController<TwoAxisPosition>requiredController aus createOverlayUrlState, verteilt mit urlIndexTwoAxisKey. Seine Position – ein Objekt { outer, inner } – entscheidet, ob der Player offen ist und wo er öffnet; das Overlay schreibt bei jeder Navigation und beim Schließen zurück.

Interfaces StoryItem, AuthorInfo und 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[];
}

Kontexttypen der Template-Slots

Jeder Kontexttyp wird exportiert, sodass ein Slot dort annotiert werden kann, wo der Compiler ihn nicht ableiten kann – StoriesSlideContext, StoriesHeaderContext, StoriesFooterContext, StoriesProgressBarContext, StoriesNavigationContext, StoriesLoadingContext, StoriesErrorContext und StoriesGroupPreviewContext, dazu StoriesNavigationActions, DesktopLayout, ChromePlacement und 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';

CSS-Klassen

Die Klassennamen sind einfach (nicht gekapselt), sodass ein Stylesheet, das nach @reelkit/angular-stories-player/styles.css geladen wird, jede davon überschreiben kann. Sie sind identisch mit @reelkit/react-stories-player und @reelkit/vue-stories-player, daher funktioniert ein Stylesheet für ein Binding auch für die anderen. Für Farben, Größen und z-indizes nutzt du besser die Custom Properties unter Theming.

KlasseKomponenteBeschreibung
.rk-stories-overlayOverlayFester Vollbild-Hintergrund (Hintergrund, z-index)
.rk-stories-overlay--carouselOverlayOverlay mit sichtbarem Desktop-Karussell
.rk-stories-overlay--slidingOverlayEin Karussell-Slide läuft; die Fläche des Players ist verborgen
.rk-stories-swipe-wrapperOverlayWrapper für Wischen zum Schließen (enthält Navigations-Buttons und Fläche)
.rk-stories-containerOverlayAbgerundete Story-Fläche (Position, Overflow)
.rk-stories-ui-layerOverlayEine Ebene für die Bedienelemente über dem Player (Header, Fortschritt, Herzen). Mit chromePlacement="group" stattdessen eine in jedem Gruppen-Slide (Header, Fortschritt), und die Herzen liegen direkt im Player
.rk-stories-ui-layer--hiddenOverlayAusgeblendeter Zustand der Bedienelemente (umgeschaltet durch hideUIOnPause)
.rk-stories-errorOverlayFehlerzustand (zentriertes Symbol und Text)
.rk-stories-error-textOverlayText der Fehlermeldung
.rk-stories-nav-btnOverlayDesktop-Pfeil zurück/weiter
.rk-stories-progress-barProgressBarWrapper zur Positionierung des Canvas-Fortschrittsbalkens
.rk-stories-slide-wrapperOverlayEine Gruppe von Stories (äußerer Slide)
.rk-stories-storyOverlayEine einzelne Story (Wurzel des inneren Slides)
.rk-stories-imageImageStorySlideRandloses Bild (object-fit: cover)
.rk-stories-videoVideoStorySlideContainer des Video-Slides
.rk-stories-video-elementVideoStorySlideDas geteilte <video>-Element
.rk-stories-video-posterVideoStorySlidePosterbild, sichtbar bis zum Start der Wiedergabe
.rk-stories-video-poster--visibleVideoStorySlideSichtbarer Zustand des Posterbilds (vor der Wiedergabe)
.rk-stories-heartHeartAnimationDas Herz beim Doppeltippen
.rk-stories-headerStoryHeaderHeader-Zeile (Avatar, Name, Zeit, Steuerelemente)
.rk-stories-header--hiddenStoryHeaderAusgeblendeter Zustand des Headers (Input visible auf false gesetzt)
.rk-stories-header-avatarStoryHeaderAvatarbild des Autors
.rk-stories-header-nameStoryHeaderName des Autors
.rk-stories-header-verifiedStoryHeaderContainer des Verifizierungsabzeichens
.rk-stories-header-timeStoryHeaderText mit der vergangenen Zeit
.rk-stories-header-actionsStoryHeaderSteuerelemente rechts (Ton, Pause, Schließen)
.rk-stories-header-btnStoryHeaderButtons für Pause, Ton und Schließen
.rk-stories-header-btn--desktopStoryHeaderDer Pause-Button, nur ab einer Breite über 768px sichtbar
.rk-stories-header-spinnerStoryHeaderLade-Spinner, während die aktive Story lädt
.rk-stories-ringStoriesRingRunder Avatar mit dem Verlaufsring
.rk-stories-ring--activeStoriesRingRing mit noch anzusehenden Stories (rotierender Verlauf)
.rk-stories-ring-avatarStoriesRingDas Avatarbild im Ring
.rk-stories-ring-listStoriesRingListHorizontale Reihe von Ringen
.rk-stories-ring-list-itemStoriesRingListSpalte aus Ring und Name
.rk-stories-ring-list-nameStoriesRingListName des Autors unter jedem Ring
.rk-stories-carouselCarouselEbene der Vorschaukarten hinter dem Player
.rk-stories-carousel--instantCarouselErster Frame eines Slides, ohne Übergang angeordnet
.rk-stories-cardCarouselEine positionierte Vorschaukarte
.rk-stories-card--centerCarouselKarte im mittleren Platz während eines Slides
.rk-stories-card--hiddenCarouselKarte jenseits der zwei, die auf einer Seite sichtbar sind
.rk-stories-card-buttonCarouselStandardinhalt der Karte (der Button, der die Gruppe öffnet)
.rk-stories-card-imageCarouselDas Vorschaubild der Karte (Posterbild oder Bild)
.rk-stories-card-frameCarouselEin als Kartenvorschau gezeichnetes Slide-Template, verkleinert
.rk-stories-card-scrimCarouselAbdunkelnde Ebene über der Vorschau
.rk-stories-card-infoCarouselSpalte aus Ring, Name des Autors und Zeit
.rk-stories-card-nameCarouselName des Autors
.rk-stories-card-timeCarouselText mit der vergangenen Zeit

Theming

Jede Farbe, Größe, Dauer und jeder z-index ist eine --rk-stories-*-Custom-Property auf :root, daher ist ein neues Branding ein Stylesheet und keine Änderung an der Komponente. Die Tokens werden mit den Playern für React und Vue geteilt.

TokenStandardSteuert
--rk-stories-overlay-bg#000Farbe des Vollbild-Hintergrunds
--rk-stories-overlay-z9999z-index des Overlays
--rk-stories-swipe-gap16pxAbstand zwischen Navigations-Buttons und Story-Fläche
--rk-stories-container-radius12pxAbgerundete Ecken der Story-Fläche (Desktop)
--rk-stories-container-radius-mobile0Ecken der Story-Fläche auf dem Smartphone
--rk-stories-ui-z15z-index der Ebene für die Bedienelemente (Header, Fortschrittsbalken)
--rk-stories-ui-transition200msDauer der Überblendung, wenn hideUIOnPause umschaltet
--rk-stories-top-shade-height120pxHöhe des oberen Verlaufs hinter dem Header
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Farbe des oberen Verlaufs
--rk-stories-error-z5z-index des Fehler-Panels
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Hintergrundverlauf des Fehlerzustands
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Farbe von Fehlersymbol und Text
--rk-stories-error-gap12pxAbstand zwischen Fehlersymbol und Text
--rk-stories-error-text-size13pxSchriftgröße der Fehlermeldung
--rk-stories-nav-z20z-index der Desktop-Navigations-Buttons
--rk-stories-nav-size44pxGröße der Desktop-Buttons zurück/weiter
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Hintergrund der Desktop-Navigations-Buttons
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Hover-Hintergrund der Desktop-Navigations-Buttons
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Symbolfarbe der Desktop-Navigations-Buttons
--rk-stories-nav-fg-hover#fffSymbolfarbe der Desktop-Navigations-Buttons beim Hover
--rk-stories-nav-transition150msHover-Übergang der Desktop-Navigations-Buttons
--rk-stories-video-bg#000Hintergrund der Letterbox hinter <video>
--rk-stories-video-poster-transition200msDauer, mit der das Posterbild ausblendet, wenn das Video startet
--rk-stories-progress-bar-z10z-index des Fortschrittsbalkens
--rk-stories-progress-bar-padding8px 8px 0Abstand um den Fortschrittsbalken
--rk-stories-card-bg#262626Hintergrund der Karussell-Karte, sichtbar, wenn es keinen Vorschau-Frame gibt
--rk-stories-card-radius8pxEckenradius der Karussell-Karte
--rk-stories-card-scrimrgba(0, 0, 0, 0.45)Abdunkelung über einer seitlichen Karte
--rk-stories-card-fg#fffTextfarbe der Karte
--rk-stories-card-name-size14pxSchriftgröße des Autorennamens auf der Karte
--rk-stories-card-time-fgrgba(255, 255, 255, 0.7)Textfarbe der vergangenen Zeit auf der Karte
--rk-stories-card-time-size13pxSchriftgröße der vergangenen Zeit auf der Karte
--rk-stories-card-gap6pxAbstand zwischen Ring, Name und Zeit auf einer Karte
--rk-stories-card-transition300msDauer eines Karussell-Slides
--rk-stories-ring-spin-duration4sRotationsdauer eines Rings mit noch anzusehenden Stories
--rk-stories-ring-active-scale0.95Skalierung des Rings beim Drücken
--rk-stories-ring-active-gradientconic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433)Was ein Ring mit noch anzusehenden Stories zeigt, ein beliebiges CSS-Bild oder eine Farbe. Auf einer hellen Seite setzt du es auf .rk-stories-ring-list, nicht auf :root, damit das Karussell des Players sein eigenes behält
--rk-stories-ring-viewed-colorrgba(255, 255, 255, 0.25)Was ein vollständig angesehener Ring zeigt, gedacht für eine dunkle Seite. Auf einer hellen Seite setzt du es auf .rk-stories-ring-list, nicht auf :root
--rk-stories-ring-gradientnoneIntern: Der Ring schreibt es bei jedem Rendern und verweist damit auf die beiden Tokens oben, außer der Ring erhält gradientColors oder viewedColor
--rk-stories-ring-list-gap12pxAbstand zwischen den Ringen in der Liste
--rk-stories-ring-list-padding12pxInnenabstand um die Ringliste
--rk-stories-ring-list-item-gap4pxAbstand zwischen einem Ring und seinem Namen
--rk-stories-ring-list-name-size12pxSchriftgröße des Autorennamens unter jedem Ring
--rk-stories-header-z5z-index des Headers
--rk-stories-header-top18pxVertikaler Abstand des Headers vom oberen Rand der Story
--rk-stories-header-padding12px 16pxInnenabstand der Header-Zeile
--rk-stories-header-gap8pxAbstand zwischen den Elementen der Header-Zeile
--rk-stories-header-transition200msDauer, mit der der Header beim Verbergen ausblendet
--rk-stories-header-avatar-size32pxBreite und Höhe des Avatars
--rk-stories-header-name-fg#fffFarbe des Autorennamens
--rk-stories-header-name-size14pxSchriftgröße des Autorennamens
--rk-stories-header-name-weight600Schriftstärke des Autorennamens
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Textfarbe der vergangenen Zeit
--rk-stories-header-time-size12pxSchriftgröße der vergangenen Zeit
--rk-stories-header-actions-gap8pxAbstand zwischen den Header-Buttons
--rk-stories-header-btn-fg#fffSymbolfarbe der Header-Aktionen (Schließen, Ton, Pause)
--rk-stories-header-btn-padding4pxInnenabstand der Header-Buttons
--rk-stories-header-spinner-size20pxGröße des Lade-Spinners
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)Farbe der Spur des Lade-Spinners
--rk-stories-header-spinner-fg#fffFarbe des Bogens des Lade-Spinners
--rk-stories-header-spinner-duration0.8sEine Umdrehung des Spinners
--rk-stories-heart-z20z-index des Herzens beim Doppeltippen
--rk-stories-heart-duration800msDauer der Animation zum Einblenden und Ausblenden

Füge das folgende Snippet in ein Stylesheet ein, das nach @reelkit/angular-stories-player/styles.css geladen wird.

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

Barrierefreiheit

Das Root-Element des Overlays ist ein modaler Dialog (role="dialog", aria-modal="true"). Setze ariaLabel, um die Ansage für Screenreader zu ändern; standardmäßig lautet sie „Stories player“.

Das Overlay fängt beim Öffnen den Fokus ein und gibt ihn beim Schließen an den Auslöser zurück. Tab und Shift+Tab wechseln zwischen den fokussierbaren Elementen darin und springen an beiden Enden um; Fokus, der entkommt (Klick außerhalb, programmatischer Fokus), wird zurückgeholt. Das Scrollen der Seite bleibt gesperrt, solange der Player offen ist. Umgesetzt mit captureFocusForReturn und createFocusTrap aus @reelkit/core, re-exportiert von @reelkit/angular.

Beim Desktop-Karussell ist jede Standardkarte ein Button mit der Beschriftung „Open stories by“ und dem Namen des Autors. Die Karten folgen in der Tab-Reihenfolge nach den Steuerelementen des Players und werden aus ihr herausgenommen, solange ein Slide läuft; eine Karte, die ihre Gruppe öffnet, gibt den Fokus an den Dialog zurück, statt ihn auf der Seite fallen zu lassen.

Serverseitiges Rendering

Der Player rendert nichts, bis er geöffnet ist, und liest während der Erstellung keine Browser-API, daher ist er unter Angular SSR sicher. Was schon gesehen wurde, wird nach dem Mount gelesen statt während des Renderns – das hält das Rendering auf dem Server und das erste Rendering im Client übereinstimmend.

Tastenkürzel

TasteAktion
ArrowLeftVorherige Story. Auf der ersten Story einer Gruppe die vorherige Gruppe, dort geöffnet, wo sie verlassen wurde; bei der ersten Gruppe nichts.
ArrowRightNächste Story. Auf der letzten Story einer Gruppe die nächste Gruppe; bei der letzten Gruppe schließt sich der Player.
EscapeSchließt den Player