Angular Stories Player

Оверлей сторіс у стилі Instagram для Angular на @reelkit/angular-stories-player. Використовує той самий рушій @reelkit/stories-core, що й плеєри React і Vue, тож поведінка, класи CSS і токени теми однакові.

Переглянути демо →

Features

Вкладена навігація
Дотик гортає сторіс, свайп змінює автора
Відеосторіс
Автовідтворення з перемиканням звуку
Автоперехід
Налаштовувана тривалість для кожної сторіс
3D-переходи
Куб, фліп, згасання, зум, слайд
Смуга прогресу
Сегментований прогрес на canvas
Зображення та відео
Підтримує обидва типи медіа
Шаблонні слоти
Вісім областей на ng-template
Подвійний дотик
Анімація серця на подвійний дотик
Навігація на десктопі
Бічні стрілки на великих екранах
Кільця сторіс
Аватари з кільцем у стилі Instagram
Узагальнені типи
Розширте StoryItem власними полями
Токени теми
Ребрендинг лише через --rk-stories-*
Стан в URL
Посилання ?story=група.сторіс, якими можна ділитися
Стан переглянутого
Переглянуті кільця та відновлення переживають перезавантаження

Встановлення

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

Зареєструйте таблицю стилів один раз:

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"]
Іконки

Типовий заголовок і стрілки навігації використовують lucide-angular для іконок. Якщо ви віддаєте перевагу іншій бібліотеці іконок, замініть їх через шаблонні слоти rkStoriesHeader і rkStoriesNavigation.

Швидкий старт

rk-stories-overlay рендерить повноекранний плеєр. Поєднайте його з rk-stories-ring-list, щоб отримати точки входу в стилі Instagram: кільце емітує (selected) з індексом групи, який хост зберігає як initialGroupIndex, перш ніж задати isOpen. Плеєр ніколи не закривається сам: він емітує (closed), а хост повертає isOpen у 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);
  }
}
Де рендериться оверлей

На відміну від плеєрів React і Vue, які переносять свій оверлей у кінець document.body, rk-stories-overlay рендериться там, де ви його розмістили, і накриває екран через position: fixed. Не кладіть його всередину предка з transform, perspective, filter або will-change: transform: такий предок стає блоком-контейнером оверлея, тож плеєр отримує розмір предка замість вікна й обрізається його overflow. Предок, що створює власний контекст накладання, також обмежує, наскільки високо --rk-stories-overlay-z може підняти плеєр. Натомість токени --rk-stories-*, задані на будь-якому предку, доходять до плеєра.

Стан в URL

Переглянути демо →

RkStoriesUrlOverlayComponent — окремий компонент, стан відкриття якого живе в адресному рядку. Обидві осі їдуть в одному параметрі — ?story=<group>.<story> — тож на відкриту історію є посилання, яким можна поділитися, зберегти в закладки й закрити кнопкою «Назад». Створіть контролер через createOverlayUrlState і urlIndexTwoAxisKey, а тоді передайте його як controller.

Вбудовані ключі

Історії двовісні, тож розгортайте в контролер двовісний ключ: urlIndexTwoAxisKey (автор і історія за позицією) або urlStableIdTwoAxisKey (автор за стабільним id) — обидва реекспортовані з @reelkit/angular-stories-player. Дивіться посібник зі стану в URL і 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`);
  }
}
  • Відкриття додає один запис в історію. Перехід між історіями і зміна автора лише замінюють його, тож N переходів не додають записів, а один крок назад завжди закриває плеєр. «Назад» закриває, а не гортає історії.
  • Внутрішня навігація теж їде в URL. Перехід між історіями одного автора оновлює ?story=2.n, тож пряме посилання веде саме на потрібну історію.
  • «Назад» закриває лише тоді, коли плеєр відкрили всередині застосунку — це посилання додало запис. У спільного посилання, відкритого в новій вкладці, історії позаду немає, тож «Назад» веде геть із сайту; кнопка ✕ або Escape прибирає параметр на місці й лишає вас тут.
  • Параметр, що не називає ані автора, ані історії — застаріла закладка, виправлене руками значення, історія за межами групи — прибирається з URL, а не відкриває сусідню.
  • Застосунок із роутером має передати адаптер на його основі, щоб роутер лишався єдиним джерелом правди про навігацію. createRouterUrlAdapter із @reelkit/angular/ng-router-url-adapter — готовий адаптер для роутера Angular; запис в історію повз роутер лишає його локацію застарілою, і наступна навігація губить параметр.

Стабільні посилання. Типово автор адресується за позицією, тож збережене ?story=2.0 відкриє іншу людину, щойно стрічку перевпорядкують. Адресуйте його за стабільним id — outerCodec записує id в URL, а outerLocator знаходить, де він стоїть. Передайте innerCodec разом із innerLocate та innerIdentify, щоб зробити те саме з історією всередині її групи; якщо їх не передати, половина з історією лишається звичайним локальним індексом.

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

Нескінченні стрічки. Посторінкове завантаження — справа outerLocator і не залежить від кодека. locate синхронний, тож відповідає лише про вже завантажені групи: спільне посилання на групу 400 у стрічці, де завантажено 20, нічого не знайде. locateAsync — запасний шлях, який викликають лише тоді, коли locate не впорався; історія потім обмежується межами тієї групи, на якій усе зупинилося.

Той самий locateAsync, зовнішня вісь

Це той самий пейджер locateAsync, який приймають одновісні ключі — у двовісному ключі він їде у вашому outerLocator, тож сторінки гортає вісь авторів, а історія лишається локальним індексом усередині знайденої групи.

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;
    },
  },
});
  • Поки locateAsync у процесі, плеєр лишається закритим, а параметр — недоторканим, тож пряме посилання переживає запит. null або відмова прибирає параметр.
  • Відповідь, що приходить після зміни URL, після закриття або після демонтажу, відкидається — повільний запит не відкриє історію, якої ніхто не просив.
  • Повний перелік опцій createOverlayUrlState є в довіднику Angular API, а покроковий розбір — у посібнику з Angular.

Запам’ятовування переглянутого

Кільця показують градієнт, доки групу не переглянуто до кінця, а група знову відкривається на першій сторіс, якої глядач ще не бачив. Обидва ефекти дає один createStoriesViewedStateController, створений один раз там, де живе стрічка, і переданий списку кілець і плеєру як viewed: вони читають сховище, стежать за ним і самі записують показане. Збережений запис називає автора й сторіс за id, тож переживає перевпорядкування стрічки, а той самий контролер працює і в React, і у 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);
  }
}
  • Початкова сторіс рахується. Сторіс, яка вже на екрані, позначається переглянутою в момент відкриття плеєра, тож якщо відкрити групу з однією сторіс і закрити її, група вважається переглянутою.
  • Гортання далі теж відновлює. resumeStoryIndex питають для кожної групи, до якої вперше дійшли в цьому сеансі, включно з тією, на якій плеєр відкривається, — хіба що initialStoryIndex прямо називає сторіс; група, яку вже гортали, відкривається там, де її залишили. Явний input resumeStoryIndex перемагає відповідь контролера.
  • Посилання все одно перемагає. З rk-stories-url-overlay місце відкриття плеєра визначає параметр, хоч би що було збережено. Скрізь інше вирішує колбек відновлення.
  • Лічильник — це позиція, а не підсумок. Запис називає найдальшу сторіс, до якої дійшли, тож додана до переглянутої групи сторіс знову запалює її кільце, а видалена з середини — скорочує лічильник.
  • Сховище підключається. Передайте storage: createSessionStorageAdapter(), щоб забувати все після закриття вкладки, або власний StorageAdapter. Дві відкриті вкладки синхронізуються через подію storage браузера.
  • Плеєр, створений лише в момент відкриття, читає надто пізно. rk-stories-overlay читає сховище, щойно з’являється, ще закритим, і так само робить rk-stories-ring-list. Обгорнутий у @if (open) оверлей такого моменту не має: викличте attachViewedState(() => this.viewed) у конструкторі хоста — це під’єднує стан, доки живе хост. Опції та нижчорівневі складові, з яких це побудовано, описано на сторінці Stories Core.

Задайте desktopLayout="carousel", щоб розкласти плеєр як у вебпереглядачі Instagram: активна сторіс лишається по центру у звичному розмірі, а до двох сусідніх груп з кожного боку стоять меншими притемненими картками. Клік по картці відкриває цю групу, а картки ковзають, поки група змінюється. Передайте той самий контролер viewed, що й списку кілець, і картка переглянутої групи отримає приглушене кільце.

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 {}
  • Телефони лишають звичайний плеєр. На ширині до 768px картки не рендеряться, а свайпи використовують groupTransition, як і з типовою розкладкою 'single'. Зміна розміру через цю межу перемикає розкладку на льоту. Поки картки показано, групи ковзають, а не повертаються, хоч би що казав groupTransition.
  • Кожна картка показує, де група відкриється: сторіс, на якій її залишили в цьому сеансі, інакше ту, яку повертає resumeStoryIndex. Показується постер відео або зображення. Сторіс без жодного з них, як-от текстову на градієнті, малює ваш шаблон rkStoriesSlide у розмірі плеєра й зменшує; відео без постера показує тло картки. Такий попередній перегляд отримує isActive: false, а його колбеки життєвого циклу нічого не роблять, тож він не може керувати таймером сторіс на екрані. Усе, що слайд робить під час створення, як-от відтворення чи аналітику, тримайте за isActive.
  • Таймер чекає на слайд. Сторіс не запускає таймер, і (storyViewed) для неї не емітується, доки картки не завершать рух.
  • Кожен спосіб змінити групу ковзає: клік по картці, StoriesApi.goToGroup, nextGroup і prevGroup та стрілки на клавіатурі за межею групи. Сенсорний свайп рухає сам плеєр, тож ковзання поверх нього не додається.
  • Перестилізувати картку можна слотом rkStoriesGroupPreview, показаним у розділі Власні картки каруселі. Плеєр і далі розміщує, масштабує та рухає картку.

Смуга прогресу й заголовок у кожній групі

Типово одна смуга прогресу й один заголовок лежать над плеєром і перемикаються на нову групу, щойно вона змінилася. Задайте chromePlacement="group", щоб кожна група мала власні всередині свого слайда: тоді обидва повертаються разом із групою, як в 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 {}
  • Сусідня група показує, де вона зупинилася. Поки плеєр повертається до неї, її смуга й заголовок показують сторіс, з якої вона відкриється, ще без жодного перегляду.
  • Група, яку покидають, лишається як була. Її смуга тримає прогрес до кінця повороту, тож досмотрена група лишається заповненою, а не порожніє посеред повороту.
  • Шаблони рендеряться для кожної групи на екрані. Контексти rkStoriesProgressBar і rkStoriesHeader містять groupIndex та isActive; для сусідньої групи isActive дорівнює false, а сигнали прогресу не змінюються.
  • Шаблон rkStoriesHeader лежить в області свайпу. Дотик до нього перемикає сторіс, якщо він не влучив у button, посилання чи елемент із role="button", тож усі елементи керування в ньому мають бути саме такими.
  • Десктопна карусель посеред ковзання не змінюється. Поки картки ковзають, плеєр прихований, тож вибір видно, щойно група відкрилася.

Шаблонні слоти

Вісім директив ng-template замінюють будь-яку частину плеєра. Кожна отримує типізований контекст, де кожне значення має ім'я — те саме, що в слотах React і Vue, — тож let-author="author" бере автора у слоті заголовка. Головне значення також є неявним, тож і сам let-author робить те саме. Слот, який ви не надали, лишає типове відображення.

ДирективаКласКонтекстОпис
rkStoriesErrorRkStoriesErrorDirective{ $implicit: story, story, storyIndex, groupIndex }Замінює вбудовану панель "Content unavailable", намальовану над активною сторіс
rkStoriesFooterRkStoriesFooterDirective{ $implicit: story, story, author, storyIndex }Додає підвал під активною сторіс; плеєр власного не малює
rkStoriesGroupPreviewRkStoriesGroupPreviewDirective{ $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen }Заповнює картку каруселі на десктопі; плеєр і далі розміщує, масштабує та рухає її
rkStoriesHeaderRkStoriesHeaderDirective{ $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose }Замінює типовий заголовок (автор, пауза, звук, закриття). З chromePlacement="group" він лежить в області свайпу; див. Смуга прогресу й заголовок у кожній групі.
rkStoriesLoadingRkStoriesLoadingDirective{ $implicit: story, story, storyIndex, groupIndex }Замінює типовий індикатор завантаження в заголовку, поки активна сторіс вантажиться
rkStoriesNavigationRkStoriesNavigationDirective{ onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } }Замінює обидві десктопні стрілки
rkStoriesProgressBarRkStoriesProgressBarDirective{ $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive }Замінює смугу прогресу на canvas. activeIndex і progress — сигнали ядра, підключіть їх через toAngularSignal.
rkStoriesSlideRkStoriesSlideDirective{ $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }Замінює типові слайди зображення та відео
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 експортує всі вісім класів одним масивом — для компонента, який імпортує кілька з них.

Кожен слот рендериться через власний інжектор плеєра, тож компонент, намальований усередині слота, знаходить провайдери плеєра: rk-video-story-slide у шаблоні слайда отримує той самий стан звуку, який записує кнопка в заголовку.

Власні слайди

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

Власний слайд повідомляє плеєру, як ідуть справи з його вмістом, через колбеки в контексті rkStoriesSlide. Плеєр реагує на них лише для сторіс на екрані; виклик із будь-якого іншого слайда ігнорується.

КолбекКоли
onReadyВміст готовий (зображення завантажено, відео грає). Таймер прогресу запускається або чекає, доки завершиться ковзання каруселі.
onWaitingВміст зависає (відео буферизується посеред відтворення). З’являється індикатор, таймер стає на паузу.
onErrorВміст не завантажився. Показується панель помилки, таймер стає на паузу.
onDurationReadyПовідомте справжню тривалість медіа в мілісекундах, наприклад із метаданих відео, щоб таймер їй відповідав. duration, заданий на сторіс, перемагає це значення.
onEndedМедіа завершилося саме, наприклад відео дограло до кінця. Плеєр переходить до наступної сторіс, не чекаючи таймера.

rk-image-story-slide емітує (loaded) і (failed), а rk-video-story-slide — (playbackStarted), (buffering), (failed), (durationReady) і (finished), тож шаблону слайда, який їх обгортає, лишається лише передати кожну подію до відповідного колбека.

Кеш попереднього завантаження

Вбудовані RkImageStorySlideComponent і RkVideoStorySlideComponent завантажують наступну сторіс у фоні. Коли глядач доходить до попередньо завантаженої сторіс, вміст з’являється миттєво, без індикатора завантаження. URL, який один раз не завантажився, теж запам’ятовується, тож повторне відкриття зламаної сторіс одразу показує помилку замість нової спроби.

Власна навігація

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>

Власна смуга прогресу

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>

Власні стани завантаження та помилки

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>

Переходи

groupTransition керує 3D-ефектом під час зміни автора. Імпортуйте функції переходів із @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;
}

Подвійний дотик і вподобайки

Подвійний дотик програє анімацію серця — миттєвий візуальний відгук. (doubleTapped) віддає індекс групи та сторіс, щоб ви зберегли вподобайку у власному стані. Сам плеєр стану вподобайок не тримає.

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

Налаштування анімації серця

Змініть швидкість токеном --rk-stories-heart-duration (див. Theming). Для кольору, розміру або щоб приховати серце повністю, націльтеся на клас .rk-stories-heart. RkHeartAnimationComponent експортовано для окремого використання.

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

Вбудовану анімацію серця поки не можна замінити слотом. Перестилізуйте її через CSS або сховайте за допомогою display: none і запускайте власну анімацію з (doubleTapped). Якщо вам потрібен слот для серця, напишіть у GitHub Issues.

StoriesApi

Візьміть API з (apiReady) для імперативного керування. Плеєр перебудовується під час кожного відкриття, тож дескриптор, збережений із попереднього відкриття, застарілий — користуйтеся найновішим.

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

Методи

МетодТипОпис
nextStory()() => voidПерейти до наступної сторіс у поточній групі
prevStory()() => voidПовернутися до попередньої сторіс у поточній групі
nextGroup()() => voidПерейти до наступної групи автора
prevGroup()() => voidПерейти до попередньої групи автора
goToGroup(index)(index: number) => voidПерейти до конкретної групи за індексом
pause()() => voidПризупинити автоперехід і таймер прогресу
resume()() => voidВідновити автоперехід і таймер прогресу

Підкомпоненти

Експортовані складові — щоб збирати їх усередині слотів або використовувати окремо.

КомпонентЩо це
RkCanvasProgressBarComponentСегментована смуга прогресу на canvas, намальована прямо із сигналів таймера. Обробляє рухоме вікно для груп із багатьма сторіс. З [live]="false" малюється лише при зміні сигналів і після зміни розміру, без циклу анімації. Розміри й кольори — окремі inputs: gap, barHeight, minSegmentWidth, bgColor, fillColor.
RkHeartAnimationComponentСерце подвійного дотику. Емітує (completed), коли анімація завершується, щоб батьківський компонент його прибрав.
RkImageStorySlideComponentСлайд зображення на весь екран із object-fit: cover, повідомляє про завантаження та помилку через outputs.
RkStoriesRingComponentКруглий аватар із градієнтним кільцем. Два стани: якщо є що дивитися — обертається, переглянуте до кінця — рівне. Емітує (clicked).
RkStoriesRingListComponentГоризонтальний ряд кілець з іменами авторів, по одному на групу. Емітує (selected) з індексом групи.
RkStoryHeaderComponentТиповий заголовок: аватар, ім’я, позначка верифікації, відносний час, пауза, звук, закриття та індикатор завантаження. Кнопки паузи й звуку з'являються лише з [showPauseButton] і [showSoundButton], як їх вмикає плеєр для власного заголовка.
RkVideoStorySlideComponentВідеослайд на єдиному спільному елементі <video> — заради неперервності звуку в iOS. Обробляє автовідтворення, постери та повідомлення про тривалість.

RkStoriesCarouselComponent теж експортовано разом із типом CarouselSlide, але оверлей володіє всіма значеннями, з яких малює карусель, — щоб змінити вигляд картки, беріть слот rkStoriesGroupPreview, а не монтуйте карусель самотужки.

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

Власні типи сторіс

Розширте StoryItem власними полями й типізуйте ним групи:

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

Реекспорти

Усе, що потрібно споживачеві, надходить із цього пакета, тож прямий імпорт із @reelkit/core ніколи не потрібен.

ЗвідкиРеекспортовано
@reelkit/angularSoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, а також типи OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter
@reelkit/stories-corecreateStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, а також типи вмісту StoryItem, AuthorInfo, StoriesGroup, MediaType

attachViewedState теж експортовано — для компонента, який володіє контролером стану переглянутого й хоче тримати його активним, доки живе цей компонент.

Inputs RkStoriesOverlayComponent

rk-stories-overlay

InputТипТипове значенняОпис
ariaLabelstring'Stories player'Доступна назва діалогової області; програми читання з екрана озвучують її, коли оверлей відкривається
chromePlacement'overlay' | 'group''overlay'Де живуть смуга прогресу й заголовок. 'overlay' малює одну копію над плеєром; 'group' малює по одній у кожному слайді групи, тож обидва повертаються разом із групою. Тип експортується як ChromePlacement.
defaultImageDurationnumber5000Тривалість автопереходу для сторіс-зображення, у мілісекундах
desktopLayout'single' | 'carousel''single'Розкладка на десктопі. 'carousel' показує сусідні групи як картки попереднього перегляду поруч з активною сторіс і рухає їх між групами. Телефони завжди показують сторіс саму. Тип експортовано як DesktopLayout.
enableKeyboardbooleantrueВмикає навігацію з клавіатури (стрілки ліворуч/праворуч, Escape)
groupsStoriesGroup<T>[]requiredГрупи сторіс для показу. Групи або сторіс, додані за відкритого плеєра, підхоплюються без втрати місця, на якому був глядач.
groupTransitionTransitionTransformFncubeTransitionПерехід між авторами. Ігнорується, поки показано десктопну карусель, яка рухається слайдом.
hideUIOnPausebooleantrueХоває смугу прогресу та заголовок, поки довге натискання тримає паузу
initialGroupIndexnumber0Індекс (від нуля) групи, з якої відкривається плеєр
initialStoryIndexnumberвідновлення, інакше 0Індекс (від нуля) сторіс, з якої відкривається плеєр. Явно вказана перемагає будь-що запам’ятоване; якщо не вказати, початкова група відновлюється, як будь-яка інша.
innerTransitionDurationnumber200Тривалість згасання між сторіс одного автора, у мілісекундах
isOpenbooleanrequiredРендерить плеєр і блокує прокручування body, поки true
minSegmentWidthnumber8Найменша ширина сегмента прогресу, після якої смуга починає прокручувати вікно сегментів, у пікселях
resumeStoryIndex(groupIndex: number) => number—З якої сторіс група відкривається, коли до неї доходять уперше. Питається лише для групи, ще не відвіданої в цьому відкритті, і перемагає viewed.
tapZoneSplitnumber0.3Співвідношення зон дотику (0–1). Ліва частина повертає назад, решта — вперед.
viewedStoriesViewedStateController—Що глядач уже переглянув, із createStoriesViewedStateController(). Групи відновлюються на першій непереглянутій сторіс, кожна показана сторіс фіксується, а картки каруселі гасять кільце групи, переглянутої до кінця. Передайте той самий контролер до rk-stories-ring-list.

Кожен слот має ще й input — для компонента, який тримає TemplateRef сам, замість проєктувати його: slideTemplate, headerTemplate, footerTemplate, progressBarTemplate, navigationTemplate, loadingTemplate, errorTemplate і groupPreviewTemplate. Шаблон, названий так, перемагає спроєктований усередину плеєра.

Outputs RkStoriesOverlayComponent

OutputНавантаженняОпис
(apiReady)StoriesApiЕмітується, щойно плеєр готовий приймати команди, і знову під час кожного відкриття
(closed)voidЕмітується, коли глядач закриває плеєр. Плеєр лишається на місці; вирішує хост. У rk-stories-url-overlay параметр на цей момент уже очищено.
(doubleTapped){ groupIndex, storyIndex }Емітується на подвійний дотик, разом з анімацією серця
(groupChanged)numberЕмітується після зміни активної групи
(paused)voidЕмітується на довгому натисканні або на кнопці паузи в заголовку
(resumed)voidЕмітується після відпускання або на кнопці відтворення в заголовку
(storyChanged){ groupIndex, storyIndex }Емітується після зміни активної сторіс
(storyCompleted){ groupIndex, storyIndex }Емітується, коли таймер сторіс вичерпано
(storyViewed){ groupIndex, storyIndex }Емітується, коли сторіс з’являється на екрані, включно з початковою

Плеєр повідомляє про закриття, а не закривається сам, тож (closed) покриває всі виходи: кнопку ✕, свайп униз, Escape і завершення останньої сторіс останньої групи.

Inputs RkStoriesUrlOverlayComponent

rk-stories-url-overlay

Приймає всі inputs вище, крім трійці стану відкриття — isOpen, initialGroupIndex, initialStoryIndex, — які надходять від контролера.

InputТипТипове значенняОпис
controllerUrlStateController<TwoAxisPosition>requiredКонтролер із createOverlayUrlState, поєднаний з urlIndexTwoAxisKey. Його позиція — об’єкт { outer, inner } — визначає, чи плеєр відкритий і де саме він відкривається; оверлей переписує її на кожній навігації та під час закриття.

Інтерфейси StoryItem, AuthorInfo і 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[];
}

Типи контекстів шаблонних слотів

Усі типи контекстів експортовано, щоб анотувати слот там, де компілятор не може вивести тип, — StoriesSlideContext, StoriesHeaderContext, StoriesFooterContext, StoriesProgressBarContext, StoriesNavigationContext, StoriesLoadingContext, StoriesErrorContext і StoriesGroupPreviewContext, поряд зі StoriesNavigationActions, DesktopLayout, ChromePlacement і 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

Назви класів звичайні (без області видимості), тож таблиця стилів, завантажена після @reelkit/angular-stories-player/styles.css, може перевизначити будь-який із них. Вони однакові з @reelkit/react-stories-player і @reelkit/vue-stories-player, тож стилі, написані для одного біндингу, працюють і для інших. Для кольорів, розмірів і z-index краще користуйтеся кастомними властивостями з Theming.

КласКомпонентОпис
.rk-stories-overlayOverlayФіксоване тло на весь екран (колір, z-index)
.rk-stories-overlay--carouselOverlayОверлей із показаною десктопною каруселлю
.rk-stories-overlay--slidingOverlayТриває рух каруселі; полотно плеєра приховано
.rk-stories-swipe-wrapperOverlayОбгортка свайпу для закриття (тримає стрілки та полотно)
.rk-stories-containerOverlayЗаокруглене полотно сторіс (позиція, overflow)
.rk-stories-ui-layerOverlayОдин шар інтерфейсу над плеєром (заголовок, прогрес, серця). З chromePlacement="group" натомість по одному в кожному слайді групи (заголовок, прогрес), а серця лежать прямо в плеєрі
.rk-stories-ui-layer--hiddenOverlayПрихований інтерфейс (перемикає hideUIOnPause)
.rk-stories-errorOverlayСтан помилки (іконка й текст по центру)
.rk-stories-error-textOverlayТекст повідомлення про помилку
.rk-stories-nav-btnOverlayДесктопна стрілка назад/вперед
.rk-stories-progress-barProgressBarОбгортка позиціювання смуги прогресу на canvas
.rk-stories-slide-wrapperOverlayОдна група сторіс (зовнішній слайд)
.rk-stories-storyOverlayОдна сторіс (корінь внутрішнього слайда)
.rk-stories-imageImageStorySlideЗображення на весь кадр (object-fit: cover)
.rk-stories-videoVideoStorySlideКонтейнер відеослайда
.rk-stories-video-elementVideoStorySlideСпільний елемент <video>
.rk-stories-video-posterVideoStorySlideПостер, показаний до початку відтворення
.rk-stories-video-poster--visibleVideoStorySlideСтан видимого постера (до відтворення)
.rk-stories-heartHeartAnimationСерце подвійного дотику
.rk-stories-headerStoryHeaderРядок заголовка (аватар, ім’я, час, елементи керування)
.rk-stories-header--hiddenStoryHeaderПрихований заголовок (input visible задано як false)
.rk-stories-header-avatarStoryHeaderЗображення аватара автора
.rk-stories-header-nameStoryHeaderТекст імені автора
.rk-stories-header-verifiedStoryHeaderКонтейнер позначки верифікації
.rk-stories-header-timeStoryHeaderТекст відносного часу
.rk-stories-header-actionsStoryHeaderЕлементи керування праворуч (звук, пауза, закриття)
.rk-stories-header-btnStoryHeaderКнопки паузи, звуку та закриття
.rk-stories-header-btn--desktopStoryHeaderКнопка паузи, видима лише на ширині понад 768px
.rk-stories-header-spinnerStoryHeaderІндикатор завантаження, поки активна сторіс вантажиться
.rk-stories-ringStoriesRingКруглий аватар із градієнтним кільцем
.rk-stories-ring--activeStoriesRingКільце з непереглянутими сторіс (градієнт обертається)
.rk-stories-ring-avatarStoriesRingЗображення аватара всередині кільця
.rk-stories-ring-listStoriesRingListГоризонтальний ряд кілець
.rk-stories-ring-list-itemStoriesRingListКолонка з кільцем та іменем
.rk-stories-ring-list-nameStoriesRingListІм’я автора під кожним кільцем
.rk-stories-carouselCarouselШар карток попереднього перегляду позаду плеєра
.rk-stories-carousel--instantCarouselПерший кадр руху, розкладений без переходу
.rk-stories-cardCarouselОдна розміщена картка попереднього перегляду
.rk-stories-card--centerCarouselКартка в центральному слоті під час руху
.rk-stories-card--hiddenCarouselКартка за межами двох, показаних з боку
.rk-stories-card-buttonCarouselТиповий вміст картки (кнопка, що відкриває групу)
.rk-stories-card-imageCarouselЗображення попереднього перегляду на картці (постер або зображення)
.rk-stories-card-frameCarouselШаблон слайда, намальований як попередній перегляд картки, у зменшеному масштабі
.rk-stories-card-scrimCarouselПритемнення поверх попереднього перегляду
.rk-stories-card-infoCarouselКолонка з кільцем, іменем автора й часом
.rk-stories-card-nameCarouselІм’я автора
.rk-stories-card-timeCarouselТекст відносного часу

Theming

Кожен колір, розмір, тривалість і z-index — кастомна властивість --rk-stories-* на :root, тож ребрендинг — це таблиця стилів, а не зміна компонента. Токени спільні з плеєрами React і Vue.

ТокенТипове значенняЩо задає
--rk-stories-overlay-bg#000Колір повноекранного тла
--rk-stories-overlay-z9999z-index оверлея
--rk-stories-swipe-gap16pxВідступ між кнопками навігації та полотном сторіс
--rk-stories-container-radius12pxЗаокруглення полотна сторіс (десктоп)
--rk-stories-container-radius-mobile0Заокруглення полотна сторіс на телефоні
--rk-stories-ui-z15z-index шару інтерфейсу (заголовок, смуга прогресу)
--rk-stories-ui-transition200msТривалість згасання, коли перемикається hideUIOnPause
--rk-stories-top-shade-height120pxВисота верхнього градієнтного затемнення за заголовком
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)Колір верхнього градієнтного затемнення
--rk-stories-error-z5z-index панелі помилки
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)Градієнт тла стану помилки
--rk-stories-error-fgrgba(255, 255, 255, 0.5)Колір іконки й тексту помилки
--rk-stories-error-gap12pxВідстань між іконкою помилки та текстом
--rk-stories-error-text-size13pxРозмір шрифту повідомлення про помилку
--rk-stories-nav-z20z-index десктопної кнопки навігації
--rk-stories-nav-size44pxРозмір десктопної кнопки назад/вперед
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)Тло десктопної кнопки навігації
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)Тло десктопної кнопки навігації під курсором
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)Колір іконки десктопної кнопки навігації
--rk-stories-nav-fg-hover#fffКолір іконки десктопної кнопки навігації під курсором
--rk-stories-nav-transition150msПерехід десктопної кнопки навігації під курсором
--rk-stories-video-bg#000Тло-леттербокс за <video>
--rk-stories-video-poster-transition200msТривалість згасання постера на початку відтворення
--rk-stories-progress-bar-z10z-index смуги прогресу
--rk-stories-progress-bar-padding8px 8px 0Відступ навколо смуги прогресу
--rk-stories-card-bg#262626Тло картки каруселі, видиме без кадру попереднього перегляду
--rk-stories-card-radius8pxЗаокруглення кутів картки каруселі
--rk-stories-card-scrimrgba(0, 0, 0, 0.45)Притемнення бічної картки
--rk-stories-card-fg#fffКолір тексту на картці
--rk-stories-card-name-size14pxРозмір шрифту імені автора на картці
--rk-stories-card-time-fgrgba(255, 255, 255, 0.7)Колір тексту часу на картці
--rk-stories-card-time-size13pxРозмір шрифту часу на картці
--rk-stories-card-gap6pxВідстань між кільцем, іменем і часом на картці
--rk-stories-card-transition300msТривалість руху каруселі
--rk-stories-ring-spin-duration4sТривалість обертання кільця, у якому лишилося що дивитися
--rk-stories-ring-active-scale0.95Масштаб кільця під час натискання
--rk-stories-ring-active-gradientconic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433)Що показує кільце, коли лишилися історії: будь-яке CSS-зображення чи колір. На світлій сторінці задавайте його на .rk-stories-ring-list, а не на :root, щоб карусель плеєра зберегла свій
--rk-stories-ring-viewed-colorrgba(255, 255, 255, 0.25)Що показує повністю переглянуте кільце; розраховано на темну сторінку. На світлій задавайте його на .rk-stories-ring-list, а не на :root
--rk-stories-ring-gradientnoneВнутрішній: кільце записує його під час кожного рендеру й посилається на два токени вище, якщо кільцю не передали gradientColors чи viewedColor
--rk-stories-ring-list-gap12pxВідстань між кільцями у списку
--rk-stories-ring-list-padding12pxВнутрішні відступи навколо списку кілець
--rk-stories-ring-list-item-gap4pxВідстань між кільцем та іменем
--rk-stories-ring-list-name-size12pxРозмір шрифту імені автора під кільцем
--rk-stories-header-z5z-index заголовка
--rk-stories-header-top18pxВертикальний відступ заголовка від верху сторіс
--rk-stories-header-padding12px 16pxВнутрішні відступи рядка заголовка
--rk-stories-header-gap8pxВідстань між елементами рядка заголовка
--rk-stories-header-transition200msТривалість згасання заголовка, коли він ховається
--rk-stories-header-avatar-size32pxШирина й висота аватара
--rk-stories-header-name-fg#fffКолір імені автора
--rk-stories-header-name-size14pxРозмір шрифту імені автора
--rk-stories-header-name-weight600Товщина шрифту імені автора
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)Колір тексту відносного часу
--rk-stories-header-time-size12pxРозмір шрифту відносного часу
--rk-stories-header-actions-gap8pxВідстань між кнопками заголовка
--rk-stories-header-btn-fg#fffКолір іконок дій у заголовку (закриття, звук, пауза)
--rk-stories-header-btn-padding4pxВнутрішній відступ кнопки заголовка
--rk-stories-header-spinner-size20pxРозмір індикатора завантаження
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)Колір доріжки індикатора завантаження
--rk-stories-header-spinner-fg#fffКолір дуги індикатора завантаження
--rk-stories-header-spinner-duration0.8sОдин оберт індикатора
--rk-stories-heart-z20z-index серця подвійного дотику
--rk-stories-heart-duration800msТривалість появи й згасання серця

Вставте фрагмент нижче в таблицю стилів, завантажену після @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;
}

Accessibility

Корінь оверлея — модальний діалог (role="dialog", aria-modal="true"). Встановіть ariaLabel, щоб змінити те, що озвучує програма читання з екрана; типово це "Stories player".

Оверлей захоплює фокус при відкритті й повертає його на елемент, що відкрив, при закритті. Tab і Shift+Tab обходять фокусовані елементи всередині, замикаючись на краях; фокус, що вислизає (клік поза межами, програмний фокус), повертається назад. Прокручування body заблоковано, доки плеєр відкритий. Реалізовано через captureFocusForReturn і createFocusTrap з @reelkit/core, які реекспортує @reelkit/angular.

З десктопною каруселлю кожна типова картка — кнопка з підписом "Open stories by" та іменем автора. Картки йдуть після елементів керування плеєра в порядку табуляції й випадають із нього, поки триває рух каруселі; картка, що відкриває свою групу, повертає фокус діалогу, а не скидає його на сторінку.

Рендеринг на сервері

Плеєр нічого не рендерить, доки не відкритий, і не читає жодного API браузера під час конструювання, тож він безпечний для Angular SSR. Стан переглянутого читається після монтування, а не під час рендеру, і саме це тримає серверний рендер і перший клієнтський рендер у згоді.

Клавіатурні скорочення

КлавішаДія
ArrowLeftПопередня сторіс. На першій сторіс групи — попередня група, відкрита там, де її залишили; на першій групі нічого не відбувається.
ArrowRightНаступна сторіс. На останній сторіс групи — наступна група; на останній групі плеєр закривається.
EscapeЗакрити плеєр