Angular Stories Player
Оверлей сторіс у стилі Instagram для Angular на @reelkit/angular-stories-player. Використовує той самий рушій @reelkit/stories-core, що й плеєри React і Vue, тож поведінка, класи CSS і токени теми однакові.
Features
Встановлення
npm install @reelkit/angular-stories-player @reelkit/angular lucide-angularЗареєструйте таблицю стилів один раз:
// 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.
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.
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, щоб зробити те саме з історією всередині її групи; якщо їх не передати, половина з історією лишається звичайним локальним індексом.
// ?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, тож сторінки гортає вісь авторів, а історія лишається локальним індексом усередині знайденої групи.
// 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.
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прямо називає сторіс; група, яку вже гортали, відкривається там, де її залишили. Явний inputresumeStoryIndexперемагає відповідь контролера. - Посилання все одно перемагає. З
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, що й списку кілець, і картка переглянутої групи отримає приглушене кільце.
// '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.
// 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 робить те саме. Слот, який ви не надали, лишає типове відображення.
| Директива | Клас | Контекст | Опис |
|---|---|---|---|
rkStoriesError | RkStoriesErrorDirective | { $implicit: story, story, storyIndex, groupIndex } | Замінює вбудовану панель "Content unavailable", намальовану над активною сторіс |
rkStoriesFooter | RkStoriesFooterDirective | { $implicit: story, story, author, storyIndex } | Додає підвал під активною сторіс; плеєр власного не малює |
rkStoriesGroupPreview | RkStoriesGroupPreviewDirective | { $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen } | Заповнює картку каруселі на десктопі; плеєр і далі розміщує, масштабує та рухає її |
rkStoriesHeader | RkStoriesHeaderDirective | { $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose } | Замінює типовий заголовок (автор, пауза, звук, закриття). З chromePlacement="group" він лежить в області свайпу; див. Смуга прогресу й заголовок у кожній групі. |
rkStoriesLoading | RkStoriesLoadingDirective | { $implicit: story, story, storyIndex, groupIndex } | Замінює типовий індикатор завантаження в заголовку, поки активна сторіс вантажиться |
rkStoriesNavigation | RkStoriesNavigationDirective | { onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } } | Замінює обидві десктопні стрілки |
rkStoriesProgressBar | RkStoriesProgressBarDirective | { $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive } | Замінює смугу прогресу на canvas. activeIndex і progress — сигнали ядра, підключіть їх через toAngularSignal. |
rkStoriesSlide | RkStoriesSlideDirective | { $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded } | Замінює типові слайди зображення та відео |
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 у шаблоні слайда отримує той самий стан звуку, який записує кнопка в заголовку.
Власні слайди
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, який один раз не завантажився, теж запам’ятовується, тож повторне відкриття зламаної сторіс одразу показує помилку замість нової спроби.
Власна навігація
// 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>Власна смуга прогресу
// 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>Власні стани завантаження та помилки
// 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>Власні картки каруселі
// 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:
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) віддає індекс групи та сторіс, щоб ви зберегли вподобайку у власному стані. Сам плеєр стану вподобайок не тримає.
// 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 експортовано для окремого використання.
: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) для імперативного керування. Плеєр перебудовується під час кожного відкриття, тож дескриптор, збережений із попереднього відкриття, застарілий — користуйтеся найновішим.
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, а не монтуйте карусель самотужки.
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 власними полями й типізуйте ним групи:
// 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/angular | SoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, а також типи OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter |
@reelkit/stories-core | createStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, а також типи вмісту StoryItem, AuthorInfo, StoriesGroup, MediaType |
attachViewedState теж експортовано — для компонента, який володіє контролером стану переглянутого й хоче тримати його активним, доки живе цей компонент.
Inputs RkStoriesOverlayComponent
rk-stories-overlay
| Input | Тип | Типове значення | Опис |
|---|---|---|---|
ariaLabel | string | 'Stories player' | Доступна назва діалогової області; програми читання з екрана озвучують її, коли оверлей відкривається |
chromePlacement | 'overlay' | 'group' | 'overlay' | Де живуть смуга прогресу й заголовок. 'overlay' малює одну копію над плеєром; 'group' малює по одній у кожному слайді групи, тож обидва повертаються разом із групою. Тип експортується як ChromePlacement. |
defaultImageDuration | number | 5000 | Тривалість автопереходу для сторіс-зображення, у мілісекундах |
desktopLayout | 'single' | 'carousel' | 'single' | Розкладка на десктопі. 'carousel' показує сусідні групи як картки попереднього перегляду поруч з активною сторіс і рухає їх між групами. Телефони завжди показують сторіс саму. Тип експортовано як DesktopLayout. |
enableKeyboard | boolean | true | Вмикає навігацію з клавіатури (стрілки ліворуч/праворуч, Escape) |
groups | StoriesGroup<T>[] | required | Групи сторіс для показу. Групи або сторіс, додані за відкритого плеєра, підхоплюються без втрати місця, на якому був глядач. |
groupTransition | TransitionTransformFn | cubeTransition | Перехід між авторами. Ігнорується, поки показано десктопну карусель, яка рухається слайдом. |
hideUIOnPause | boolean | true | Ховає смугу прогресу та заголовок, поки довге натискання тримає паузу |
initialGroupIndex | number | 0 | Індекс (від нуля) групи, з якої відкривається плеєр |
initialStoryIndex | number | відновлення, інакше 0 | Індекс (від нуля) сторіс, з якої відкривається плеєр. Явно вказана перемагає будь-що запам’ятоване; якщо не вказати, початкова група відновлюється, як будь-яка інша. |
innerTransitionDuration | number | 200 | Тривалість згасання між сторіс одного автора, у мілісекундах |
isOpen | boolean | required | Рендерить плеєр і блокує прокручування body, поки true |
minSegmentWidth | number | 8 | Найменша ширина сегмента прогресу, після якої смуга починає прокручувати вікно сегментів, у пікселях |
resumeStoryIndex | (groupIndex: number) => number | — | З якої сторіс група відкривається, коли до неї доходять уперше. Питається лише для групи, ще не відвіданої в цьому відкритті, і перемагає viewed. |
tapZoneSplit | number | 0.3 | Співвідношення зон дотику (0–1). Ліва частина повертає назад, решта — вперед. |
viewed | StoriesViewedStateController | — | Що глядач уже переглянув, із 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 | Тип | Типове значення | Опис |
|---|---|---|---|
controller | UrlStateController<TwoAxisPosition> | required | Контролер із createOverlayUrlState, поєднаний з urlIndexTwoAxisKey. Його позиція — об’єкт { outer, inner } — визначає, чи плеєр відкритий і де саме він відкривається; оверлей переписує її на кожній навігації та під час закриття. |
Інтерфейси StoryItem, AuthorInfo і StoriesGroup
import type {
StoryItem,
AuthorInfo,
StoriesGroup,
StoriesApi,
DesktopLayout,
} from '@reelkit/angular-stories-player';
interface StoryItem {
id: string;
mediaType: 'image' | 'video';
src: string;
poster?: string;
duration?: number;
aspectRatio?: number;
createdAt?: string | Date;
}
interface AuthorInfo {
id: string;
name: string;
avatar: string;
verified?: boolean;
}
interface StoriesGroup<T extends StoryItem = StoryItem> {
author: AuthorInfo;
stories: T[];
}Типи контекстів шаблонних слотів
Усі типи контекстів експортовано, щоб анотувати слот там, де компілятор не може вивести тип, — StoriesSlideContext, StoriesHeaderContext, StoriesFooterContext, StoriesProgressBarContext, StoriesNavigationContext, StoriesLoadingContext, StoriesErrorContext і StoriesGroupPreviewContext, поряд зі StoriesNavigationActions, DesktopLayout, ChromePlacement і StoriesApi:
// Every slot context in one place. Every value is named, by the names the
// react and vue slots use; the main one is also what `let-x` with no name
// binds.
import type {
StoriesSlideContext,
StoriesHeaderContext,
StoriesFooterContext,
StoriesProgressBarContext,
StoriesNavigationContext,
StoriesGroupPreviewContext,
StoriesLoadingContext,
StoriesErrorContext,
} from '@reelkit/angular-stories-player';Класи CSS
Назви класів звичайні (без області видимості), тож таблиця стилів, завантажена після @reelkit/angular-stories-player/styles.css, може перевизначити будь-який із них. Вони однакові з @reelkit/react-stories-player і @reelkit/vue-stories-player, тож стилі, написані для одного біндингу, працюють і для інших. Для кольорів, розмірів і z-index кращ е користуйтеся кастомними властивостями з Theming.
| Клас | Компонент | Опис |
|---|---|---|
.rk-stories-overlay | Overlay | Фіксоване тло на весь екран (колір, z-index) |
.rk-stories-overlay--carousel | Overlay | Оверлей із показаною десктопною каруселлю |
.rk-stories-overlay--sliding | Overlay | Триває рух каруселі; полотно плеєра приховано |
.rk-stories-swipe-wrapper | Overlay | Обгортка свайпу для закриття (тримає стрілки та полотно) |
.rk-stories-container | Overlay | Заокруглене полотно сторіс (позиція, overflow) |
.rk-stories-ui-layer | Overlay | Один шар інтерфейсу над плеєром (заголовок, прогрес, серця). З chromePlacement="group" натомість по одному в кожному слайді групи (заголовок, прогрес), а серця лежать прямо в плеєрі |
.rk-stories-ui-layer--hidden | Overlay | Прихований інтерфейс (перемикає hideUIOnPause) |
.rk-stories-error | Overlay | Стан помилки (іконка й текст по центру) |
.rk-stories-error-text | Overlay | Текст повідомлення про помилку |
.rk-stories-nav-btn | Overlay | Десктопна стрілка назад/вперед |
.rk-stories-progress-bar | ProgressBar | Обгортка позиціювання смуги прогресу на canvas |
.rk-stories-slide-wrapper | Overlay | Одна група сторіс (зовнішній слайд) |
.rk-stories-story | Overlay | Одна сторіс (корінь внутрішнього слайда) |
.rk-stories-image | ImageStorySlide | Зображення на весь кадр (object-fit: cover) |
.rk-stories-video | VideoStorySlide | Контейнер відеослайда |
.rk-stories-video-element | VideoStorySlide | Спільний елемент <video> |
.rk-stories-video-poster | VideoStorySlide | Постер, показаний до початку відтворення |
.rk-stories-video-poster--visible | VideoStorySlide | Стан видимого постера (до відтворення) |
.rk-stories-heart | HeartAnimation | Серце подвійного дотику |
.rk-stories-header | StoryHeader | Рядок заголовка (ават ар, ім’я, час, елементи керування) |
.rk-stories-header--hidden | StoryHeader | Прихований заголовок (input visible задано як false) |
.rk-stories-header-avatar | StoryHeader | Зображення аватара автора |
.rk-stories-header-name | StoryHeader | Текст імені автора |
.rk-stories-header-verified | StoryHeader | Контейнер позначки верифікації |
.rk-stories-header-time | StoryHeader | Текст відносного часу |
.rk-stories-header-actions | StoryHeader | Елементи керування праворуч (звук, пауза, закриття) |
.rk-stories-header-btn | StoryHeader | Кнопки паузи, звуку та закриття |
.rk-stories-header-btn--desktop | StoryHeader | Кнопка паузи, видима лише на ширині понад 768px |
.rk-stories-header-spinner | StoryHeader | Індикатор завантаження, поки активна сторіс вантажиться |
.rk-stories-ring | StoriesRing | Круглий аватар із градієнтним кільцем |
.rk-stories-ring--active | StoriesRing | Кільце з непереглянути ми сторіс (градієнт обертається) |
.rk-stories-ring-avatar | StoriesRing | Зображення аватара всередині кільця |
.rk-stories-ring-list | StoriesRingList | Горизонтальний ряд кілець |
.rk-stories-ring-list-item | StoriesRingList | Колонка з кільцем та іменем |
.rk-stories-ring-list-name | StoriesRingList | Ім’я автора під кожним кільцем |
.rk-stories-carousel | Carousel | Шар карток попереднього перегляду позаду плеєра |
.rk-stories-carousel--instant | Carousel | Перший кадр руху, розкладений без переходу |
.rk-stories-card | Carousel | Одна розміщена картка попереднього перегляду |
.rk-stories-card--center | Carousel | Картка в центральному слоті під час руху |
.rk-stories-card--hidden | Carousel | Картка за межами двох, показаних з боку |
.rk-stories-card-button | Carousel | Типовий вміст картки (кнопка, що відкриває групу) |
.rk-stories-card-image | Carousel | Зображення попереднього перегляду на картці (постер аб о зображення) |
.rk-stories-card-frame | Carousel | Шаблон слайда, намальований як попередній перегляд картки, у зменшеному масштабі |
.rk-stories-card-scrim | Carousel | Притемнення поверх попереднього перегляду |
.rk-stories-card-info | Carousel | Колонка з кільцем, іменем автора й часом |
.rk-stories-card-name | Carousel | Ім’я автора |
.rk-stories-card-time | Carousel | Текст відносного часу |
Theming
Кожен колір, розмір, тривалість і z-index — кастомна властивість --rk-stories-* на :root, тож ребрендинг — це таблиця стилів, а не зміна компонента. Токени спільні з плеєрами React і Vue.
| Токен | Типове значення | Що задає |
|---|---|---|
--rk-stories-overlay-bg | #000 | Колір повноекранного тла |
--rk-stories-overlay-z | 9999 | z-index оверлея |
--rk-stories-swipe-gap | 16px | Відступ між кнопками навігації та полотном сторіс |
--rk-stories-container-radius | 12px | Заокруглення полотна сторіс (десктоп) |
--rk-stories-container-radius-mobile | 0 | Заокруглення полотна сторіс на телефоні |
--rk-stories-ui-z | 15 | z-index шару інтерфейсу (заголовок, смуга прогресу) |
--rk-stories-ui-transition | 200ms | Тривалість згасання, коли перемикається hideUIOnPause |
--rk-stories-top-shade-height | 120px | Висота верхнього градієнтного затемнення за заголовком |
--rk-stories-top-shade-bg | linear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%) | Колір верхнього градієнтного затемнення |
--rk-stories-error-z | 5 | z-index панелі помилки |
--rk-stories-error-bg | linear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%) | Градієнт тла стану помилки |
--rk-stories-error-fg | rgba(255, 255, 255, 0.5) | Колір іконки й тексту помилки |
--rk-stories-error-gap | 12px | Відстань між іконкою помилки та текстом |
--rk-stories-error-text-size | 13px | Розмір шрифту повідомлення про помилку |
--rk-stories-nav-z | 20 | z-index десктопної кнопки навігації |
--rk-stories-nav-size | 44px | Розмір десктопної кнопки назад/вперед |
--rk-stories-nav-bg | rgba(255, 255, 255, 0.1) | Тло десктопної кнопки навігації |
--rk-stories-nav-bg-hover | rgba(255, 255, 255, 0.2) | Тло десктопної кнопки навігації під курсором |
--rk-stories-nav-fg | rgba(255, 255, 255, 0.7) | Колір іконки десктопної кнопки навігації |
--rk-stories-nav-fg-hover | #fff | Колір іконки десктопної кнопки навігації під курсором |
--rk-stories-nav-transition | 150ms | Перехід десктопної кнопки навігації під курсором |
--rk-stories-video-bg | #000 | Тло-леттербокс за <video> |
--rk-stories-video-poster-transition | 200ms | Тривалість згасання постера на початку відтворення |
--rk-stories-progress-bar-z | 10 | z-index смуги прогресу |
--rk-stories-progress-bar-padding | 8px 8px 0 | Відступ навколо смуги прогресу |
--rk-stories-card-bg | #262626 | Тло картки каруселі, видиме без кадру попереднього перегляду |
--rk-stories-card-radius | 8px | Заокруглення кутів картки каруселі |
--rk-stories-card-scrim | rgba(0, 0, 0, 0.45) | Притемнення бічної картки |
--rk-stories-card-fg | #fff | Колір тексту на картці |
--rk-stories-card-name-size | 14px | Розмір шрифту імені автора на картці |
--rk-stories-card-time-fg | rgba(255, 255, 255, 0.7) | Колір тексту часу на картці |
--rk-stories-card-time-size | 13px | Розмір шрифту часу на картці |
--rk-stories-card-gap | 6px | Відстань між кільцем, іменем і часом на картці |
--rk-stories-card-transition | 300ms | Тривалість руху каруселі |
--rk-stories-ring-spin-duration | 4s | Тривалість обертання кільця, у якому лишилося що дивитися |
--rk-stories-ring-active-scale | 0.95 | Масштаб кільця під час натискання |
--rk-stories-ring-active-gradient | conic-gradient(from 180deg, #f09433, #e6683c, #dc2743, #cc2366, #bc1888, #f09433) | Що показує кільце, к оли лишилися історії: будь-яке CSS-зображення чи колір. На світлій сторінці задавайте його на .rk-stories-ring-list, а не на :root, щоб карусель плеєра зберегла свій |
--rk-stories-ring-viewed-color | rgba(255, 255, 255, 0.25) | Що показує повністю переглянуте кільце; розраховано на темну сторінку. На світлій задавайте його на .rk-stories-ring-list, а не на :root |
--rk-stories-ring-gradient | none | Внутрішній: кільце записує його під час кожного рендеру й посилається на два токени вище, якщо кільцю не передали gradientColors чи viewedColor |
--rk-stories-ring-list-gap | 12px | Відстань між кільцями у списку |
--rk-stories-ring-list-padding | 12px | Внутрішні відступи навколо списку кілець |
--rk-stories-ring-list-item-gap | 4px | Відстань між кільцем та іменем |
--rk-stories-ring-list-name-size | 12px | Р озмір шрифту імені автора під кільцем |
--rk-stories-header-z | 5 | z-index заголовка |
--rk-stories-header-top | 18px | Вертикальний відступ заголовка від верху сторіс |
--rk-stories-header-padding | 12px 16px | Внутрішні відступи рядка заголовка |
--rk-stories-header-gap | 8px | Відстань між елементами рядка заголовка |
--rk-stories-header-transition | 200ms | Тривалість згасання заголовка, коли він ховається |
--rk-stories-header-avatar-size | 32px | Ширина й висота аватара |
--rk-stories-header-name-fg | #fff | Колір імені автора |
--rk-stories-header-name-size | 14px | Розмір шрифту імені автора |
--rk-stories-header-name-weight | 600 | Товщина шрифту імені автора |
--rk-stories-header-time-fg | rgba(255, 255, 255, 0.6) | Колір тексту відносного часу |
--rk-stories-header-time-size | 12px | Розмір шрифту відносного часу |
--rk-stories-header-actions-gap | 8px | Відстань між кнопками заголовка |
--rk-stories-header-btn-fg | #fff | Колір іконок дій у заголовку (закриття, звук, пауза) |
--rk-stories-header-btn-padding | 4px | Внутрішній відступ кнопки заголовка |
--rk-stories-header-spinner-size | 20px | Розмір індикатора завантаження |
--rk-stories-header-spinner-track | rgba(255, 255, 255, 0.3) | Колір доріжки індикатора завантаження |
--rk-stories-header-spinner-fg | #fff | Колір дуги індикатора завантаження |
--rk-stories-header-spinner-duration | 0.8s | Один оберт індикатора |
--rk-stories-heart-z | 20 | z-index серця подвійного дотику |
--rk-stories-heart-duration | 800ms | Тривалість появи й згасання серця |
Вставте фрагмент нижче в таблицю стилів, завантажену після @reelkit/angular-stories-player/styles.css.
/* Loaded after @reelkit/angular-stories-player/styles.css. Every token has a
default on :root, so override only what you want to change. The names
match the React and Vue players, so a theme ports between bindings. */
:root {
--rk-stories-overlay-bg: #0f172a;
--rk-stories-container-radius: 24px;
--rk-stories-nav-bg: rgba(99, 102, 241, 0.35);
--rk-stories-nav-bg-hover: rgba(168, 85, 247, 0.65);
--rk-stories-nav-fg: #fff;
--rk-stories-header-name-fg: #fef3c7;
--rk-stories-header-time-fg: rgba(254, 243, 199, 0.7);
--rk-stories-ring-spin-duration: 2s;
--rk-stories-heart-duration: 600ms;
--rk-stories-card-bg: #1e293b;
--rk-stories-card-transition: 260ms;
}
/* Rings on a light page. Set on the ring list, not :root, so the rings of the
player's own carousel keep the colours made for its dark backdrop. */
.rk-stories-ring-list {
--rk-stories-ring-viewed-color: #d4d4d8;
}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 | Закрити плеєр |