Angular Stories Player

@reelkit/angular-stories-player を使った Angular 向けの Instagram 風ストーリープレイヤーオーバーレイです。React・Vue のプレイヤーと同じ @reelkit/stories-core エンジンを共有しているため、挙動も CSS クラスもテーマトークンも同じです。

ライブデモを見る →

機能

ネストしたナビゲーション
タップでストーリーを送り、スワイプでユーザーを切り替え
動画ストーリー
自動再生とサウンド切り替え
自動送り
ストーリーごとに設定できる表示時間
3D 遷移
キューブ、フリップ、フェード、ズーム、スライド
プログレスバー
canvas で描く分割プログレス
画像と動画
どちらのメディアにも対応
テンプレートスロット
ng-template による 8 つの領域
ダブルタップのいいね
ダブルタップでハートのアニメーション
デスクトップのナビ
大きい画面では左右の矢印ボタン
ストーリーリング
Instagram 風のリング付きアバター
ジェネリック型
StoryItem を独自のフィールドで拡張
テーマトークン
--rk-stories-* だけでブランドを変更
URL の状態
共有できる ?story=group.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 が全画面のプレイヤーを描画します。Instagram 風の入口には rk-stories-ring-list を組み合わせてください。リングはグループのインデックスとともに (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);
  }
}
オーバーレイが描画される場所

オーバーレイを document.body の末尾へ移す React と Vue のプレーヤーと違い、rk-stories-overlay は置いた場所に描画され、position: fixed で画面を覆います。transform、perspective、filter、will-change: transform を持つ祖先の中には置かないでください。その祖先がオーバーレイの包含ブロックになり、プレーヤーはビューポートではなくその祖先の大きさになり、祖先の overflow で切り取られます。独自のスタッキングコンテキストを作る祖先も、--rk-stories-overlay-z でプレーヤーをどこまで持ち上げられるかを制限します。その代わり、どの祖先に設定した --rk-stories-* トークンもプレーヤーに届きます。

URL の状態

ライブデモを見る →

RkStoriesUrlOverlayComponent は、開いているかどうかをアドレスバーに置く別コンポーネントです。2 つの軸が 1 つのパラメーター ?story=<group>.<story> に乗るので、再生中のストーリーには共有・ブックマークでき、戻るボタンで閉じられるリンクが付きます。createOverlayUrlState と urlIndexTwoAxisKey でコントローラーを作り、controller として渡してください。

組み込みのキー

ストーリーは 2 軸なので、2 軸のキーをコントローラーに展開します。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`);
  }
}
  • 開くと履歴が 1 件 積まれます。ストーリー間の移動もユーザーの切り替えも、その 1 件を 置き換える だけなので、何回操作しても履歴は増えず、戻る 1 回で必ずプレイヤーが閉じます。戻るは閉じる操作であって、ストーリーを 1 つ戻す操作ではありません。
  • 内側の移動も URL に乗ります。 同じユーザーのストーリーを進めると ?story=2.n が更新されるので、ディープリンクはそのストーリーちょうどに着きます。
  • 戻るで閉じるのは、アプリ内からプレイヤーを開いたときだけです。そのリンクが履歴を 1 件積んでいるからです。新しいタブで直接開いた共有リンクには背後に履歴がないので、ブラウザーの戻るはサイトの外へ出ます。✕ ボタンや Escape はその場でパラメーターを消し、ページにとどまります。
  • ユーザーもストーリーも指していないパラメーター(古いブックマーク、手で書き換えた値、グループの終わりを超えた番号)は、隣を開くのではなく URL から取り除かれます。
  • ルーターを使うアプリは、ルーター側のアダプターを渡してください。ナビゲーションの正となる場所をルーターに一本化するためです。@reelkit/angular/ng-router-url-adapter の createRouterUrlAdapter が Angular ルーター用の既製アダプターです。ルーターの背後で履歴を書くと location が古いままになり、次のナビゲーションでパラメーターが落ちます。

安定したリンク。 ユーザーは既定では位置で指定されるため、ブックマークした ?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,
});

無限フィード。 ページングは codec とは独立した outerLocator の仕事です。locate は同期なので、すでに読み込まれたグループにしか答えられません。20 件しか読み込んでいないフィードで 400 番目のグループを指す共有リンクは空振りします。locateAsync はその控えで、locate が外したときだけ呼ばれます。ストーリーはその後、落ち着いた先のグループに合わせて範囲が取り直されます。

同じ locateAsync、外側の軸

これは 1 軸のキーが受け取るのと同じ locateAsync ページャーです。2 軸のキーでは渡した 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 ガイドにあります。

見たものを覚えておく

リングはグループを最後まで見るまでグラデーションを表示し、グループは視聴者がまだ見ていない最初のストーリーから開き直します。どちらも 1 つの 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);
  }
}
  • 開いたときのストーリーも数えます。 プレイヤーが開くときに画面に出ているストーリーは視聴済みとして報告されるので、ストーリーが 1 つのグループを開いて閉じれば視聴済みになります。
  • スワイプで先へ進んでも再開します。 resumeStoryIndex は、このセッションで初めて到達したすべてのグループについて問い合わせられます。プレイヤーが開くグループも含みますが、initialStoryIndex がストーリーを明示している場合は除きます。すでにスワイプで通ったグループは、離れたところから開き直します。明示した resumeStoryIndex 入力はコントローラーの答えより優先されます。
  • リンクはそれでも優先されます。 rk-stories-url-overlay では、保存内容にかかわらずパラメーターがプレイヤーの開く場所を決めます。それ以外の場所では再開のコールバックが決めます。
  • 数は位置であって、集計ではありません。 エントリーは到達したいちばん先のストーリーを指すので、視聴済みのグループにストーリーを追加するとリングは再び光り、途中のストーリーを削除すると数は短くなります。
  • ストレージは差し替えられます。 タブを閉じたら忘れるには storage: createSessionStorageAdapter() を、あるいは独自の StorageAdapter を渡してください。開いている 2 つのタブは、ブラウザーの storage イベントで同期されます。
  • 開いたときにだけ作られるプレイヤーでは、読み込みが遅すぎます。 rk-stories-overlay は、閉じている間でも存在した時点でストアを読み、rk-stories-ring-list も同様です。@if (open) で包んだオーバーレイにはその機会がないので、ホストのコンストラクターで attachViewedState(() => this.viewed) を呼んでください。ホストが存在する間、接続が続きます。オプションと、これを構成する低レベルの部品は Stories Core のページにあります。

desktopLayout="carousel" を指定すると、Instagram の Web ビューアーのようなレイアウトになります。表示中のストーリーは通常の大きさのまま中央にとどまり、両側に最大 2 つずつ隣のグループが小さく暗めのカードとして並びます。カードをクリックするとそのグループが開き、グループが変わる間カードは横へスライドします。リングリストが受け取るのと同じ 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 まではカードは描画されず、スワイプはデフォルトの 'single' レイアウトと同じく groupTransition を使います。その幅をまたいでリサイズすると、その場で切り替わります。カードが表示されている間、グループは groupTransition の指定にかかわらず回転せずスライドします。
  • 各カードは、そのグループが開く場所をプレビューします:このセッションで離れたときのストーリー、なければ resumeStoryIndex が返すものです。動画のポスターか画像を表示します。どちらもないストーリー(グラデーション上のテキストのストーリーなど)は、rkStoriesSlide テンプレートでプレイヤーの大きさに描いて縮小します。ポスターのない動画はカードの背景を表示します。このプレビューは isActive: false を受け取り、ライフサイクルのコールバックは何もしないので、画面上のストーリーのタイマーを動かすことはできません。スライドが作られたときに行う処理(再生や分析など)は isActive の後ろに置いてください。
  • タイマーはスライドを待ちます。 カードの移動が終わるまで、ストーリーはタイマーを開始せず、(storyViewed) も発行されません。
  • グループを変えるすべての方法でスライドします:カードのクリック、StoriesApi.goToGroup、nextGroup と prevGroup、そしてグループの端を越える矢印キーです。タッチのスワイプはプレイヤー自体を動かすので、その上にスライドは加わりません。
  • カードの見た目を変えるには rkStoriesGroupPreview スロットを使います。カルーセルカードのカスタマイズを参照してください。カードの配置・拡大縮小・スライドは引き続きプレイヤーが行います。

グループごとのプログレスバーとヘッダー

デフォルトでは、プログレスバーとヘッダーが 1 つずつプレーヤーの上にあり、グループが切り替わった後で新しいグループの表示に変わります。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" を持つ要素の上なら切り替わらないので、ヘッダー内の操作要素はすべてそのいずれかにしてください。
  • デスクトップのカルーセルでは、スライド中の見た目は変わりません。 カードがスライドしている間はプレーヤーが隠れるため、この設定はグループが開いてから反映されます。

テンプレートスロット

8 つの 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 は 8 つのクラスをまとめた配列として公開されています。複数をインポートするコンポーネント向けです。

どのスロットもプレイヤー自身のインジェクター経由で描画されるため、スロット内に置いたコンポーネントはプレイヤーのプロバイダーを解決できます。スライドテンプレート内の 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 トークンで調整します(テーマ設定を参照)。色やサイズ、あるいはハートを完全に隠したい場合は .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

命令的に操作するときは (apiReady) から API を受け取ります。プレイヤーは開くたびに作り直されるため、前回の open で保持したハンドルは古くなります。常に最新のものを使ってください。

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" にすると、アニメーションのループを持たず、シグナルが変わったときとサイズが変わった後にだけ描画します。サイズと色は個別の input です:gap、barHeight、minSegmentWidth、bgColor、fillColor。
RkHeartAnimationComponentダブルタップのハート。アニメーションが終わると (completed) を発行するので、親が取り除けます。
RkImageStorySlideComponentobject-fit: cover の全面画像スライド。読み込みとエラーを output で通知します。
RkStoriesRingComponentグラデーションリング付きの円形アバター。未視聴が残っていれば回転し、すべて見終わると平らになります。(clicked) を発行します。
RkStoriesRingListComponent著者名付きのリングを横一列に並べたもの(グループごとに 1 つ)。グループのインデックスとともに (selected) を発行します。
RkStoryHeaderComponent既定のヘッダー。アバター、名前、認証バッジ、相対時刻、一時停止、サウンド、閉じる、読み込みスピナー。一時停止とサウンドのボタンは [showPauseButton] と [showSoundButton] を設定したときだけ表示されます。プレーヤーは自身のヘッダーでこれらを設定しています。
RkVideoStorySlideComponent共有された 1 つの <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 も公開されています。既読状態のコントローラーを自分で持つコンポーネントが、そのコンポーネントの生存期間だけ接続しておきたいときに使います。

RkStoriesOverlayComponent の入力

rk-stories-overlay

入力型既定値説明
ariaLabelstring'Stories player'ダイアログ領域のアクセシブル名。オーバーレイが開いたときにスクリーンリーダーが読み上げます
chromePlacement'overlay' | 'group''overlay'プログレスバーとヘッダーを置く場所。'overlay' はプレーヤーの上に 1 つ描き、'group' はグループのスライドごとに 1 つ描くので、両方がグループと一緒に回転します。型は ChromePlacement として公開されています。
defaultImageDurationnumber5000画像ストーリーの自動送り時間(ミリ秒)
desktopLayout'single' | 'carousel''single'デスクトップでのレイアウト。'carousel' は隣のグループを表示中のストーリーの横にプレビューカードとして並べ、グループ間をスライドします。スマートフォンでは常にストーリーだけを表示します。型は DesktopLayout として公開されています。
enableKeyboardbooleantrueキーボード操作(左右の矢印、Escape)を有効にします
groupsStoriesGroup<T>[]required表示するストーリーのグループ。プレイヤーを開いたまま追加されたグループやストーリーは、視聴者の位置を失わずに取り込まれます。
groupTransitionTransitionTransformFncubeTransitionユーザー間の遷移。デスクトップのカルーセル表示中は無視され、代わりにスライドします。
hideUIOnPausebooleantrue長押しで一時停止している間、プログレスバーとヘッダーを隠します
initialGroupIndexnumber0プレイヤーが開くグループのインデックス(0 起点)
initialStoryIndexnumber再開位置、なければ 0プレイヤーが開くストーリーのインデックス(0 起点)。明示すると記憶された位置より優先されます。省略すると、開くグループも他と同じように再開します。
innerTransitionDurationnumber200同じユーザーのストーリー間のクロスフェード時間(ミリ秒)
isOpenbooleanrequiredtrue の間プレイヤーを描画し、body のスクロールをロックします
minSegmentWidthnumber8プログレスの 1 区切りがこの幅を下回ると、バーは区切りのウィンドウをスクロールし始めます(ピクセル)
resumeStoryIndex(groupIndex: number) => number—グループに初めて到達したとき、どのストーリーから開くか。この open でまだ訪れていないグループにだけ問い合わせ、viewed より優先されます。
tapZoneSplitnumber0.3タップ領域の分割比(0〜1)。左側が戻る、残りが進むです。
viewedStoriesViewedStateController—視聴者が見たもの。createStoriesViewedStateController() から取得します。グループは最初の未視聴ストーリーから再開し、表示されたストーリーはすべて記録され、最後まで見たグループのリングはカルーセルカード上で消えます。同じコントローラーを rk-stories-ring-list にも渡してください。

各スロットには入力もあります。TemplateRef を投影せず自分で保持するコンポーネント向けで、slideTemplate、headerTemplate、footerTemplate、progressBarTemplate、navigationTemplate、loadingTemplate、errorTemplate、groupPreviewTemplate です。こうして指定したテンプレートは、プレイヤーに投影されたものより優先されます。

RkStoriesOverlayComponent の出力

出力ペイロード説明
(apiReady)StoriesApiプレイヤーが操作を受け付けられるようになった時点と、以後の open ごとに発行されます
(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、そして最後のグループの最後のストーリーが終わったときです。

RkStoriesUrlOverlayComponent の入力

rk-stories-url-overlay

上のすべての入力を受け取りますが、開閉状態の 3 つ — isOpen、initialGroupIndex、initialStoryIndex — だけはコントローラーから渡されます。

入力型既定値説明
controllerUrlStateController<TwoAxisPosition>requiredcreateOverlayUrlState に urlIndexTwoAxisKey を展開して作ったコントローラー。その position({ 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 は テーマ設定のカスタムプロパティを使うほうが適しています。

クラスコンポーネント説明
.rk-stories-overlayOverlay画面全体を覆う固定の背景(色、z-index)
.rk-stories-overlay--carouselOverlayデスクトップのカルーセルを表示しているオーバーレイ
.rk-stories-overlay--slidingOverlayカルーセルのスライド中。プレイヤーのキャンバスは隠れます
.rk-stories-swipe-wrapperOverlayスワイプで閉じるためのラッパー(矢印とキャンバスを内包)
.rk-stories-containerOverlay角丸のストーリーキャンバス(position、overflow)
.rk-stories-ui-layerOverlayプレイヤーの上に重なる 1 枚の UI レイヤー(ヘッダー、プログレス、ハート)。chromePlacement="group" では代わりに各グループのスライドの中に 1 枚ずつ(ヘッダー、プログレス)あり、ハートはプレイヤーに直接置かれます
.rk-stories-ui-layer--hiddenOverlayUI を隠した状態(hideUIOnPause が切り替えます)
.rk-stories-errorOverlayエラー状態(中央のアイコンとテキスト)
.rk-stories-error-textOverlayエラーメッセージのテキスト
.rk-stories-nav-btnOverlayデスクトップの前へ/次へ矢印
.rk-stories-progress-barProgressBarcanvas のプログレスバーを配置するラッパー
.rk-stories-slide-wrapperOverlay1 つのグループのストーリー(外側のスライド)
.rk-stories-storyOverlay1 つのストーリー(内側スライドのルート)
.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ヘッダーを隠した状態(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配置された 1 枚のプレビューカード
.rk-stories-card--centerCarouselスライド中に中央の位置にあるカード
.rk-stories-card--hiddenCarousel片側に表示される 2 枚より外にあるカード
.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相対時刻のテキスト

テーマ設定

色・サイズ・時間・z-index はすべて :root 上の --rk-stories-* カスタムプロパティなので、ブランド変更はコンポーネントの変更ではなくスタイルシートで済みます。トークンは React・Vue のプレイヤーと共通です。

トークン既定値制御するもの
--rk-stories-overlay-bg#000全画面背景の色
--rk-stories-overlay-z9999オーバーレイの z-index
--rk-stories-swipe-gap16pxナビゲーションボタンとストーリーキャンバスの間隔
--rk-stories-container-radius12pxストーリーキャンバスの角の丸み(デスクトップ)
--rk-stories-container-radius-mobile0スマートフォンでのストーリーキャンバスの角
--rk-stories-ui-z15UI レイヤー(ヘッダー、プログレスバー)の z-index
--rk-stories-ui-transition200mshideUIOnPause が切り替わるときのフェード時間
--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-z5エラーパネルの z-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-z20デスクトップのナビゲーションボタンの z-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-z10プログレスバーの z-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 の画像か色を指定できます。明るいページでは :root ではなく .rk-stories-ring-list に設定してください。プレーヤーのカルーセルの色が保たれます
--rk-stories-ring-viewed-colorrgba(255, 255, 255, 0.25)すべて見終えたリングの表示で、暗いページ向けの値です。明るいページでは :root ではなく .rk-stories-ring-list に設定してください
--rk-stories-ring-gradientnone内部用:リングが描画のたびに書き込み、リングに gradientColors や viewedColor が渡されない限り上の 2 つのトークンを参照します
--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-z5ヘッダーの z-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スピナーが 1 回転する時間
--rk-stories-heart-z20ダブルタップのハートの z-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;
}

アクセシビリティ

オーバーレイのルートはモーダルダイアログです(role="dialog"、aria-modal="true")。スクリーンリーダーの読み上げを変えるには ariaLabel を設定してください。既定は "Stories player" です。

オーバーレイは開いたときにフォーカスを取り込み、閉じたときに元の要素へ戻します。Tab と Shift+Tab は内部のフォーカス可能な要素を巡回し、両端で折り返します。外れたフォーカス(外側のクリック、プログラムによるフォーカス)は引き戻されます。プレイヤーが開いている間、body のスクロールはロックされます。@reelkit/core の captureFocusForReturn と createFocusTrap(@reelkit/angular が再エクスポート)で実装しています。

デスクトップのカルーセルでは、既定のカードはそれぞれ "Open stories by" と著者名をラベルに持つボタンです。カードはタブ順でプレイヤーの操作ボタンより後に並び、スライド中はタブ順から外れます。自分のグループを開いたカードは、フォーカスをページに落とさずダイアログへ返します。

サーバーサイドレンダリング

プレイヤーは開かれるまで何も描画せず、構築時にブラウザ API を読まないため、Angular SSR でも安全です。既読状態はレンダリング中ではなくマウント後に読むので、サーバーでのレンダリングと最初のクライアントレンダリングが食い違いません。

キーボードショートカット

キー動作
ArrowLeft前のストーリー。グループの最初のストーリーでは前のグループを、離れたところから開きます。最初のグループでは何もしません。
ArrowRight次のストーリー。グループの最後のストーリーでは次のグループへ移り、最後のグループではプレイヤーが閉じます。
Escapeプレイヤーを閉じる