Stories Player

@reelkit/react-stories-player से बना, React के लिए Instagram जैसा stories प्लेयर overlay।

लाइव डेमो देखें →

फ़ीचर

Nested नेविगेशन
stories आगे बढ़ाने के लिए टैप, ग्रुप बदलने के लिए स्वाइप
वीडियो stories
साउंड टॉगल के साथ autoplay
अपने-आप आगे बढ़ना
हर story के लिए कॉन्फ़िगर होने वाला टाइमर
3D ट्रांज़िशन
Cube, flip, fade, zoom, slide
प्रोग्रेस बार
canvas पर आधारित segmented प्रोग्रेस
इमेज और वीडियो
दोनों तरह के मीडिया संभालता है
वर्चुअलाइज़्ड
DOM में सिर्फ़ 3 स्लाइड
डबल-टैप लाइक
डबल-टैप पर दिल का एनिमेशन
डेस्कटॉप नेविगेशन
डेस्कटॉप पर chevron बटन
Story rings
Instagram जैसी avatar rings
Generic टाइप
StoryItem को कस्टम डेटा से extend करें
Render props
हर UI एलिमेंट कस्टमाइज़ करें
URL स्टेट
शेयर होने वाले ?story=group.story लिंक
देखे गए आइटम की स्टेट
देखी गई rings और वहीं से शुरू करना reload के बाद भी

इंस्टॉलेशन

bash
npm i @reelkit/react-stories-player @reelkit/react lucide-react

styles import करना न भूलें:

typescript
import '@reelkit/react-stories-player/styles.css';
आइकन

डिफ़ॉल्ट header आइकन के लिए lucide-react इस्तेमाल करता है। अगर आप कोई और आइकन लाइब्रेरी पसंद करते हैं, तो अपने देने के लिए renderHeader और renderNavigation इस्तेमाल करें।

क्विक स्टार्ट

StoriesOverlay कंपोनेंट फ़ुलस्क्रीन stories प्लेयर रेंडर करता है। Instagram जैसे entry points के लिए इसे StoriesRingList के साथ जोड़ें। StoriesGroup objects का array पास करें और isOpen से दिखना नियंत्रित करें।

tsx
import { useState } from 'react';
import {
  StoriesOverlay,
  StoriesRingList,
  createStoriesViewedStateController,
  type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';

const groups: StoriesGroup[] = [
  {
    author: {
      id: 'user-1',
      name: 'Alice',
      avatar: '/cdn/samples/avatars/avatar-06.jpg',
      verified: true,
    },
    stories: [
      {
        id: 's1-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-001.jpg',
      },
      {
        id: 's1-2',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-002.jpg',
      },
      {
        id: 's1-3',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-003.jpg',
      },
    ],
  },
  {
    author: {
      id: 'user-2',
      name: 'Bob',
      avatar: '/cdn/samples/avatars/avatar-07.jpg',
    },
    stories: [
      {
        id: 's2-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-004.jpg',
      },
      {
        id: 's2-2',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-005.jpg',
      },
    ],
  },
  {
    author: {
      id: 'user-3',
      name: 'Charlie',
      avatar: '/cdn/samples/avatars/avatar-08.jpg',
      verified: true,
    },
    stories: [
      {
        id: 's3-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-006.jpg',
      },
    ],
  },
];

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedGroup, setSelectedGroup] = useState(0);
  // What was seen: rings, resume and recording from one controller, kept in
  // localStorage across reloads.
  const [viewed] = useState(() =>
    createStoriesViewedStateController({
      storageKey: 'stories-seen',
      groups: () => groups,
    }),
  );

  const openStories = (groupIndex: number) => {
    setSelectedGroup(groupIndex);
    setIsOpen(true);
  };

  return (
    <div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
      <StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />

      <StoriesOverlay
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        groups={groups}
        initialGroupIndex={selectedGroup}
        viewed={viewed}
      />
    </div>
  );
}
StoriesPlayer.tsx
import { useState } from 'react';
import {
  StoriesOverlay,
  StoriesRingList,
  createStoriesViewedStateController,
  type StoriesGroup,
} from '@reelkit/react-stories-player';
import '@reelkit/react-stories-player/styles.css';

const groups: StoriesGroup[] = [
  {
    author: {
      id: 'user-1',
      name: 'Alice',
      avatar: '/cdn/samples/avatars/avatar-06.jpg',
      verified: true,
    },
    stories: [
      {
        id: 's1-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-001.jpg',
      },
      {
        id: 's1-2',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-002.jpg',
      },
      {
        id: 's1-3',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-003.jpg',
      },
    ],
  },
  {
    author: {
      id: 'user-2',
      name: 'Bob',
      avatar: '/cdn/samples/avatars/avatar-07.jpg',
    },
    stories: [
      {
        id: 's2-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-004.jpg',
      },
      {
        id: 's2-2',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-005.jpg',
      },
    ],
  },
  {
    author: {
      id: 'user-3',
      name: 'Charlie',
      avatar: '/cdn/samples/avatars/avatar-08.jpg',
      verified: true,
    },
    stories: [
      {
        id: 's3-1',
        mediaType: 'image',
        src: '/cdn/samples/images/stories/story-006.jpg',
      },
    ],
  },
];

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [selectedGroup, setSelectedGroup] = useState(0);
  // What was seen: rings, resume and recording from one controller, kept in
  // localStorage across reloads.
  const [viewed] = useState(() =>
    createStoriesViewedStateController({
      storageKey: 'stories-seen',
      groups: () => groups,
    }),
  );

  const openStories = (groupIndex: number) => {
    setSelectedGroup(groupIndex);
    setIsOpen(true);
  };

  return (
    <div style={{ padding: 16, background: '#0f172a', minHeight: '100vh' }}>
      <StoriesRingList groups={groups} viewed={viewed} onSelect={openStories} />

      <StoriesOverlay
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        groups={groups}
        initialGroupIndex={selectedGroup}
        viewed={viewed}
      />
    </div>
  );
}

ऐसी रिंग पंक्ति के लिए जो रीलोड के बाद भी याद रखे कि दर्शक ने क्या देखा, स्टोरीज़ ट्रे रेसिपी देखें।

URL स्टेट

लाइव डेमो देखें →

StoriesUrlOverlay एक अलग कंपोनेंट है जिसकी खुले होने की स्टेट address bar में रहती है। दोनों अक्ष एक ही parameter में चलते हैं — ?story=<group>.<story> — इसलिए चल रही story का ऐसा लिंक होता है जिसे शेयर, bookmark और back बटन से बंद किया जा सके। useOverlayUrlState और urlIndexTwoAxisKey से कंट्रोलर बनाएँ, फिर उसे controller के रूप में दें।

बिल्ट-इन keys

Stories दो-अक्ष वाली हैं, इसलिए कंट्रोलर में दो-अक्ष वाली key spread करें: urlIndexTwoAxisKey (ग्रुप और story स्थिति से) या urlStableIdTwoAxisKey (ग्रुप स्थिर id से) — दोनों @reelkit/react से भी export होती हैं। URL स्टेट गाइड और Core API देखें।

tsx
import {
  StoriesUrlOverlay,
  useOverlayUrlState,
  urlIndexTwoAxisKey,
} from '@reelkit/react-stories-player';
import { Link } from 'react-router-dom';

const stories = useOverlayUrlState({
  param: 'story',
  ...urlIndexTwoAxisKey({
    outerCount: () => groups.length,
    innerCounts: () => groups.map((g) => g.stories.length),
  }),
});

// Opening a user is a link — the overlay reads the URL and opens itself.
{groups.map((g, i) => (
  <Link key={g.author.id} to={`?story=${i}.0`}>{g.author.name}</Link>
))}

<StoriesUrlOverlay controller={stories} groups={groups} />
  • खोलने पर history में एक entry जुड़ती है। stories स्वाइप करना और यूज़र बदलना, दोनों उसे बदल देते हैं, इसलिए N नेविगेशन कोई entry नहीं जोड़ते और back का एक कदम हमेशा प्लेयर बंद कर देता है। Back बंद करता है; stories में पीछे नहीं जाता।
  • अंदरूनी नेविगेशन भी साथ चलता है। Story index ग्रुप के स्तर पर जमा नहीं रहता — किसी यूज़र की stories में आगे बढ़ने पर ?story=2.n अपडेट होता है, इसलिए सीधा लिंक ठीक उसी story पर पहुँचता है।
  • Back सिर्फ़ तब बंद करता है जब प्लेयर ऐप के अंदर से खोला गया हो — लिंक ने entry जोड़ी हो। नए tab में सीधे खोले गए शेयर किए गए लिंक के पीछे कोई history नहीं होती, इसलिए ब्राउज़र का back साइट छोड़ देता है; ✕ बटन या Escape parameter को उसी जगह हटाकर पेज पर बना रहता है।
  • किसी ग्रुप या story का नाम न लेने वाला parameter — पुराना bookmark, हाथ से बदली value, ग्रुप के अंत से आगे की story — पड़ोसी खोलने के बजाय URL से हटा दिया जाता है।

Routing वाला ऐप — adapter पास करें। router के पीछे history.pushState लिखने से उसकी location पुरानी पड़ जाती है, और उसका अगला नेविगेशन parameter गिरा देता है:

tsx
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';

const adapter = useReactRouterUrlAdapter();
const stories = useOverlayUrlState({
  param: 'story',
  adapter,
  ...urlIndexTwoAxisKey({
    outerCount: () => groups.length,
    innerCounts: () => groups.map((g) => g.stories.length),
  }),
});

<StoriesUrlOverlay controller={stories} groups={groups} />

स्थिर लिंक। ग्रुप डिफ़ॉल्ट रूप से स्थिति पर आधारित है, इसलिए फ़ीड का क्रम बदलते ही bookmark किया गया ?story=2.0 कोई दूसरा यूज़र खोलता है। इसकी जगह ग्रुप को स्थिर id से पहचानें — outerCodec id को URL में लिखता है, outerLocator ढूँढता है कि वह कहाँ है। ग्रुप के अंदर story के लिए भी यही करने को innerCodec के साथ innerLocate और innerIdentify दें; इन्हें छोड़ दें तो story वाला हिस्सा साधारण स्थानीय index ही रहता है।

tsx
const stories = useOverlayUrlState({
  param: 'story',
  ...urlIndexTwoAxisKey({
    outerCount: () => groups.length,
    innerCounts: () => groups.map((g) => g.stories.length),
    // ?story=user_42.3
    outerCodec: { decode: (raw) => raw, encode: (id) => id },
    outerLocator: {
      locate: (id) => groups.findIndex((g) => g.author.id === id),
      identify: (index) => groups[index].author.id,
    },
    // Optional: ?story=user_42.story_7 — the story by id too
    innerCodec: { decode: (raw) => raw, encode: (id) => id },
    innerLocate: (outer, id) => {
      const index = groups[outer].stories.findIndex((s) => s.id === id);
      return index === -1 ? null : index;
    },
    innerIdentify: (outer, index) => groups[outer].stories[index].id,
  }),
});

अनंत फ़ीड। पेज लोड करना outerLocator का काम है, codec से स्वतंत्र। locate synchronous है, इसलिए यह सिर्फ़ लोड हो चुके ग्रुप के लिए जवाब देता है — जिस फ़ीड में 20 ग्रुप लोड हुए हैं, उसके ग्रुप 400 का शेयर किया गया लिंक खाली लौटता है। locateAsync विकल्प है, जो सिर्फ़ locate के न मिलने पर कॉल होता है; story को उसी ग्रुप की सीमा में दोबारा बाँधा जाता है जिस पर वह पहुँचता है।

वही locateAsync, बाहरी अक्ष पर

यह वही locateAsync पेजर है जो एक-अक्ष वाली keys लेती हैं — दो-अक्ष वाली key पर यह आपके दिए outerLocator के साथ चलता है, इसलिए ग्रुप वाला अक्ष पेज लोड करता है जबकि story मिले हुए ग्रुप के अंदर स्थानीय index बनी रहती है।

tsx
const stories = useOverlayUrlState({
  param: 'story',
  ...urlIndexTwoAxisKey({
    outerCount: () => groups.length,
    innerCounts: () => groups.map((g) => g.stories.length),
    outerLocator: {
      locate: (index) => (index < groups.length ? index : null),
      identify: (index) => index,
      locateAsync: async (index) => {
        const loaded = await loadUntilGroup(index); // page up to it
        if (!loaded) return null; // exhausted — link names no group
        setGroups(loaded); // commit — the overlay renders from this state
        return index;
      },
    },
  }),
});
  • locateAsync के pending रहने तक प्लेयर बंद रहता है और parameter को छुआ नहीं जाता, इसलिए सीधा लिंक fetch के बाद भी बना रहता है। null या rejection parameter हटा देता है।
  • URL आगे बढ़ जाने, बंद होने या unmount के बाद आया जवाब फेंक दिया जाता है — धीमा fetch ऐसी story नहीं खोल सकता जो किसी ने माँगी ही नहीं।
  • useOverlayUrlState के सारे विकल्प React API रेफ़रेंस में हैं, और पूरी जानकारी React गाइड में है।

देखी गई चीज़ें याद रखना

ग्रुप पूरा देख लिए जाने तक rings पर gradient दिखता है, और ग्रुप उस पहली story पर दोबारा खुलता है जिसे दर्शक ने नहीं देखा। दोनों एक ही createStoriesViewedStateController से आते हैं, जो वहाँ एक बार बनता है जहाँ feed रहता है, और viewed के रूप में ring list और player को दिया जाता है: वे खुद store पढ़ते हैं, उसे follow करते हैं और जो दिखाया गया उसे दर्ज करते हैं। सहेजी गई entry author और story को id से नाम देती है, इसलिए feed का क्रम बदलने के बाद भी बनी रहती है, और वही कंट्रोलर Vue और Angular में भी काम करता है।

tsx
import { useRef, useState } from 'react';
import {
  StoriesOverlay,
  StoriesRingList,
  createStoriesViewedStateController,
  type StoriesGroup,
} from '@reelkit/react-stories-player';

function Feed({ groups }: { groups: StoriesGroup[] }) {
  const [open, setOpen] = useState(false);
  const [group, setGroup] = useState(0);

  // The controller reads the groups through a getter every time it needs
  // them, so keep the getter current: a ref for a plain prop, or a signal.
  const groupsRef = useRef(groups);
  groupsRef.current = groups;

  const [viewed] = useState(() =>
    createStoriesViewedStateController({
      storageKey: 'stories-seen',
      groups: () => groupsRef.current,
    }),
  );

  return (
    <>
      {/* The rings follow the controller by themselves. */}
      <StoriesRingList
        groups={groups}
        viewed={viewed}
        onSelect={(index) => {
          setGroup(index);
          setOpen(true);
        }}
      />

      {/* The player resumes, records and draws its card rings from it. */}
      <StoriesOverlay
        isOpen={open}
        onClose={() => setOpen(false)}
        groups={groups}
        initialGroupIndex={group}
        viewed={viewed}
      />
    </>
  );
}
  • खुलने वाली story गिनी जाती है। स्क्रीन पर पहले से मौजूद story mount पर देखी गई दर्ज होती है, इसलिए एक story वाला ग्रुप खोलकर बंद करने पर वह देखा हुआ माना जाता है।
  • आगे स्वाइप करने पर भी वहीं से शुरू होता है। इस सेशन में पहली बार पहुँचे हर ग्रुप के लिए resumeStoryIndex से पूछा जाता है, उस ग्रुप समेत जिस पर प्लेयर खुलता है, जब तक initialStoryIndex सीधे किसी story का नाम न ले; पहले से स्वाइप करके देखा गया ग्रुप वहीं खुलता है जहाँ छोड़ा गया था।
  • लिंक फिर भी ऊपर रहता है। StoriesUrlOverlay के साथ parameter तय करता है कि प्लेयर कहाँ खुले, चाहे जो भी सहेजा गया हो। बाकी हर जगह resume callback तय करता है।
  • गिनती एक स्थिति है, जोड़ नहीं। Entry सबसे दूर पहुँची story का नाम है, इसलिए देखे जा चुके ग्रुप में story जोड़ने पर उसकी ring फिर से जल उठती है, और बीच से एक हटाने पर गिनती घट जाती है। वैसे ही जैसे शेयर किया गया लिंक खुद को ठीक कर लेता है।
  • Storage बदला जा सकता है। tab बंद होने पर भूलने के लिए storage: createSessionStorageAdapter() पास करें, या अपना StorageAdapter। दो खुले tabs ब्राउज़र के storage इवेंट से एक-दूसरे के साथ चलते हैं।
  • सिर्फ़ खुलते समय mount होने वाला player बहुत देर से पढ़ता है। player store को ऐसे effect में पढ़ता है जो उसके बंद रहते हुए ही चलता है। {open && <StoriesOverlay … />} में वह मौका नहीं होता, इसलिए controller रखने वाले component में useAttachViewedState(viewed) कॉल करें; यह mount पर जुड़ता है और unmount पर छोड़ देता है। विकल्प, और जिन निचले स्तर के हिस्सों से यह बना है, वे Stories Core पेज पर हैं।

desktopLayout="carousel" सेट करें तो player का layout Instagram वेब जैसा हो जाता है: active story अपने सामान्य आकार में बीच में रहती है, और हर तरफ़ ज़्यादा से ज़्यादा दो पड़ोसी groups छोटे, धुंधले कार्ड के रूप में दिखते हैं। किसी कार्ड पर क्लिक करने से वह group खुलता है, और group बदलते समय कार्ड बगल में खिसकते हैं। वही viewed कंट्रोलर पास करें जो ring list को दिया जाता है, तो पूरे देखे गए group के कार्ड पर फीकी ring दिखती है। कार्ड खुद उसे follow करते हैं, इसलिए story के देखी गई दर्ज होने पर सिर्फ़ वही दोबारा बनते हैं और player दोबारा render नहीं होता।

tsx
// The same controller the ring list takes: the cards draw the same rings and
// repaint by themselves when a story is marked seen.
<StoriesOverlay
  isOpen={open}
  onClose={() => setOpen(false)}
  groups={groups}
  initialGroupIndex={group}
  desktopLayout="carousel"
  viewed={viewed}
/>;
  • फ़ोन पर player सामान्य रहता है। 768px चौड़ाई तक कार्ड render नहीं होते और swipe में groupTransition इस्तेमाल होता है, बिल्कुल डिफ़ॉल्ट 'single' layout की तरह। विंडो की चौड़ाई इस सीमा के पार बदलने पर layout तुरंत बदल जाता है।
  • हर कार्ड दिखाता है कि group कहाँ से खुलेगा: इस session में जिस story पर उसे छोड़ा गया था, वरना वह story जो resumeStoryIndex लौटाता है। कार्ड पर वीडियो का poster या खुद इमेज दिखती है। जिस story में दोनों में से कुछ नहीं है, जैसे gradient पर टेक्स्ट वाली story, उसे आपका renderSlide player के आकार में बनाता है और फिर छोटा करके दिखाया जाता है; बिना poster वाले वीडियो पर कार्ड का background दिखता है। यह preview renderSlide को isActive: false के साथ बुलाता है और सिर्फ़ एक तस्वीर है: इसके अंदर की किसी चीज़ पर न focus जाता है, न click। slide mount होते समय जो भी करती है, जैसे playback या analytics, उसे isActive के पीछे रखें।
  • टाइमर slide पूरी होने का इंतज़ार करता है। जब तक कार्ड रुक नहीं जाते, story आगे नहीं बढ़ती और उसके लिए onStoryViewed नहीं चलता।
  • group बदलने का हर तरीका slide करता है: कार्ड पर क्लिक, StoriesApi.goToGroup, nextGroup और prevGroup, और group के आख़िर से आगे arrow keys। touch swipe खुद player को खिसकाता है, इसलिए उसके ऊपर कोई slide नहीं होती।

हर ग्रुप का अपना प्रोग्रेस बार और header

डिफ़ॉल्ट रूप से एक प्रोग्रेस बार और एक header प्लेयर के ऊपर रहते हैं और ग्रुप बदल जाने के बाद नए ग्रुप पर आ जाते हैं। chromePlacement="group" सेट करने पर हर ग्रुप को अपनी slide के अंदर अपना बार और header मिलता है, और दोनों Instagram की तरह ग्रुप के साथ घूमते हैं।

tsx
// 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.
<StoriesOverlay
  isOpen={open}
  onClose={() => setOpen(false)}
  groups={groups}
  chromePlacement="group"
/>;
  • पड़ोसी ग्रुप दिखाता है कि वह कहाँ रुका है। जब प्लेयर उसकी ओर घूमता है, उसका बार और header वह story दिखाते हैं जिस पर ग्रुप खुलेगा, जिसका अभी कुछ भी नहीं चला है।
  • छोड़ा जा रहा ग्रुप जैसा था वैसा रहता है। घुमाव ख़त्म होने तक उसका बार अपनी प्रोग्रेस बनाए रखता है, इसलिए अंत तक देखा गया ग्रुप घुमाव के बीच ख़ाली नहीं होता, भरा ही रहता है।
  • कस्टम renderer स्क्रीन पर मौजूद हर ग्रुप के लिए बुलाए जाते हैं। renderProgressBar और renderHeader को groupIndex और isActive मिलते हैं; पड़ोसी ग्रुप के लिए isActive false होता है और प्रोग्रेस के signals नहीं बदलते।
  • कस्टम header swipe क्षेत्र के अंदर होता है। उस पर tap करने से story बदलती है, जब तक tap किसी button, लिंक या role="button" वाले element पर न पड़े, इसलिए उसके हर control को इन्हीं में से एक बनाएँ।
  • डेस्कटॉप कैरोसेल slide के दौरान नहीं बदलता। कार्ड slide होते समय प्लेयर छिपा रहता है, इसलिए यह विकल्प ग्रुप खुलने के बाद दिखता है।

API रेफ़रेंस

StoriesOverlayProps

StoriesOverlayProps<T>

Propटाइपडिफ़ॉल्टविवरण
isOpenbooleanज़रूरीoverlay का दिखना नियंत्रित करता है। true होने पर body का स्क्रॉल lock हो जाता है।
groupsStoriesGroup<T>[]ज़रूरीदिखाए जाने वाले story ग्रुप का array
onClose() => voidज़रूरीoverlay बंद करने का callback
ariaLabelstring'Stories player'dialog क्षेत्र का सुलभ लेबल; overlay खुलने पर स्क्रीन रीडर इसे बोलते हैं
initialGroupIndexnumber0शुरू में दिखने वाले ग्रुप का शून्य से शुरू होने वाला index
initialStoryIndexnumberresumeStoryIndex(initialGroupIndex), नहीं तो 0ग्रुप के अंदर शुरू में दिखने वाली story का शून्य से शुरू होने वाला index। इसे देने पर यह याद रखी गई किसी भी स्थिति से ऊपर रहता है; न देने पर खुलने वाला ग्रुप भी बाकी ग्रुप की तरह वहीं से शुरू होता है।
resumeStoryIndex(groupIndex: number) => number—पहली बार पहुँचने पर ग्रुप किस story पर खुले, ताकि दर्शक वहीं से आगे देखे जहाँ छोड़ा था। सिर्फ़ उस ग्रुप के लिए पूछा जाता है जो इस बार खुलने के दौरान अब तक नहीं देखा गया।
groupTransitionTransitionTransformFncubeTransitionबाहरी (ग्रुप) स्लाइडर का ट्रांज़िशन इफ़ेक्ट
defaultImageDurationnumber5000इमेज stories के अपने-आप आगे बढ़ने की डिफ़ॉल्ट अवधि, मिलीसेकंड में
tapZoneSplitnumber0.3टैप ज़ोन के बँटवारे का अनुपात (0–1)। बायाँ हिस्सा prev चलाता है, दायाँ next।
hideUIOnPausebooleantrueदेर तक दबाकर रोकने के दौरान प्रोग्रेस बार और हेडर छिपाता है
enableKeyboardbooleantrueकीबोर्ड नेविगेशन चालू करता है (बाएँ/दाएँ तीर, Escape)
innerTransitionDurationnumber200अंदरूनी (story) ट्रांज़िशन एनिमेशन की अवधि, मिलीसेकंड में
minSegmentWidthnumber8प्रोग्रेस बार के segment की पिक्सेल में न्यूनतम चौड़ाई
apiRefMutableRefObject<StoriesApi | null>-imperative StoriesApi तक पहुँचने के लिए ref
desktopLayout'single' | 'carousel''single'डेस्कटॉप layout। 'carousel' active story के बगल में पड़ोसी groups को preview कार्ड के रूप में दिखाता है और groups के बीच slide करता है। फ़ोन पर story हमेशा अकेली दिखती है। टाइप DesktopLayout नाम से export होता है।
chromePlacement'overlay' | 'group''overlay'प्रोग्रेस बार और header कहाँ रहते हैं। 'overlay' प्लेयर के ऊपर एक कॉपी बनाता है; 'group' हर ग्रुप की slide में एक बनाता है, जिससे दोनों ग्रुप के साथ घूमते हैं। टाइप ChromePlacement नाम से export होता है।
viewedStoriesViewedStateController-दर्शक ने जो देखा है, createStoriesViewedStateController() से। group अपनी पहली न देखी गई story से दोबारा शुरू होते हैं, दिखाई गई हर story दर्ज होती है, और carousel कार्ड पूरे देखे गए group की ring फीकी कर देते हैं। वही कंट्रोलर StoriesRingList को भी दें।
renderHeader(props: HeaderRenderProps<T>) => ReactNode-कस्टम header renderer। लेखक, story और pause/mute स्टेट मिलती है। chromePlacement="group" के साथ header swipe क्षेत्र के अंदर होता है; देखें हर ग्रुप का अपना प्रोग्रेस बार और header।
renderFooter(props: FooterRenderProps<T>) => ReactNode-कस्टम footer renderer। लेखक और story की जानकारी मिलती है।
renderSlide(props: SlideRenderProps<T>) => ReactNode-कस्टम स्लाइड renderer, जो डिफ़ॉल्ट इमेज/वीडियो स्लाइड की जगह लेता है।
renderNavigation(props: NavigationRenderProps) => ReactNode-कस्टम डेस्कटॉप नेविगेशन। डिफ़ॉल्ट prev/next chevron बटन की जगह लेता है।
renderProgressBar(props: ProgressBarRenderProps<T>) => ReactNode-कस्टम प्रोग्रेस बार। डिफ़ॉल्ट canvas प्रोग्रेस बार की जगह लेता है।
renderLoading(props: LoadingRenderProps<T>) => ReactNode-कस्टम लोडिंग UI renderer। न देने पर डिफ़ॉल्ट header spinner दिखता है।
renderError(props: ErrorRenderProps<T>) => ReactNode-कस्टम एरर UI renderer। न देने पर डिफ़ॉल्ट एरर आइकन overlay दिखता है।
renderGroupPreview(props: GroupPreviewRenderProps<T>) => ReactNode-कस्टम डेस्कटॉप carousel कार्ड। डिफ़ॉल्ट कार्ड content की जगह लेता है।

StoriesUrlOverlayProps

StoriesUrlOverlayProps<T>

खुली स्टेट वाली तिकड़ी — isOpen, initialGroupIndex, initialStoryIndex — को छोड़कर StoriesOverlay के सारे props लेता है; ये तीनों कंट्रोलर से आते हैं।

Propटाइपडिफ़ॉल्टविवरण
controllerUrlStateController<TwoAxisPosition>ज़रूरीurlIndexTwoAxisKey के साथ spread किए useOverlayUrlState से मिला कंट्रोलर। इसकी स्थिति — एक { outer, inner } object — तय करती है कि प्लेयर खुला है या नहीं और कहाँ खुलता है; overlay हर नेविगेशन और बंद होने पर वापस लिखता है।

Callbacks

Propटाइपविवरण
onClose() => voidप्लेयर बंद होने पर कॉल होता है। StoriesOverlay पर ज़रूरी (खुली स्टेट आपके पास है, इसलिए बंद करना आपको संभालना होगा); StoriesUrlOverlay पर वैकल्पिक, जहाँ बंद करना URL चलाता है — इसे सिर्फ़ बंद होने के बाद कुछ करने के लिए पास करें।
onStoryChange(groupIndex: number, storyIndex: number) => voidसक्रिय story बदलने पर चलता है
onGroupChange(groupIndex: number) => voidसक्रिय ग्रुप बदलने पर चलता है
onStoryViewed(groupIndex: number, storyIndex: number) => voidकिसी story के दिखने पर चलता है
onStoryComplete(groupIndex: number, storyIndex: number) => voidstory का टाइमर पूरा होने पर चलता है
onDoubleTap(groupIndex: number, storyIndex: number) => voidडबल-टैप जेस्चर पर चलता है
onPause() => voidप्लेयर रुकने पर चलता है
onResume() => voidप्लेयर फिर शुरू होने पर चलता है

ट्रांज़िशन

groupTransition prop यूज़र ग्रुप के बीच स्वाइप करते समय 3D ट्रांज़िशन इफ़ेक्ट नियंत्रित करता है। ट्रांज़िशन फ़ंक्शन @reelkit/react से import करें:

tsx
import {
  cubeTransition,   // default — 3D cube rotation
  flipTransition,   // card flip
  fadeTransition,   // crossfade
  zoomTransition,   // zoom in/out
  slideTransition,  // horizontal slide
} from '@reelkit/react';

<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  groupTransition={flipTransition}
/>

कंटेंट लोडिंग का lifecycle

हर story स्लाइड SlideRenderProps से मिले callbacks के ज़रिए अपनी लोडिंग स्टेट बताती है:

Callbackकब
onReadyकंटेंट तैयार है (इमेज लोड हुई, वीडियो चल रहा है)। प्रोग्रेस टाइमर शुरू होता है।
onWaitingकंटेंट अटक गया (वीडियो चलते-चलते buffer हो रहा है)। spinner दिखता है और टाइमर रुकता है।
onErrorकंटेंट लोड नहीं हुआ। एरर overlay दिखता है।
onDurationReadyमीडिया की असली अवधि बताएँ (जैसे वीडियो metadata से), ताकि टाइमर सही अवधि के साथ दोबारा शुरू हो।
onEndedबताएँ कि मीडिया ख़त्म हो गया (जैसे वीडियो पूरा हुआ)। अगली story पर ले जाता है।
Preloader caching

बिल्ट-इन ImageStorySlide और VideoStorySlide कंपोनेंट अगली story पीछे से पहले ही लोड कर लेते हैं। जब यूज़र पहले से लोड हुई story पर जाता है, तो कंटेंट बिना लोडिंग spinner के तुरंत दिखता है।

Render props

हर UI एलिमेंट render props से बदला जा सकता है। हर एक को सारी ज़रूरी स्टेट और callbacks वाले typed props मिलते हैं।

renderHeader

डिफ़ॉल्ट header (लेखक की जानकारी, pause/mute बटन, बंद करने का बटन) बदलें:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderHeader={({ author, story, isPaused, isMuted, isVideo, onToggleSound, onTogglePause, onClose }) => (
    <div style={{
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      padding: 16,
      display: 'flex',
      alignItems: 'center',
      gap: 8,
      zIndex: 10,
    }}>
      <img src={author.avatar} style={{ width: 32, height: 32, borderRadius: '50%' }} />
      <span style={{ color: '#fff', fontWeight: 600 }}>{author.name}</span>
      {isVideo && <button onClick={onToggleSound}>{isMuted ? 'Unmute' : 'Mute'}</button>}
      <button onClick={onClose} style={{ marginLeft: 'auto' }}>Close</button>
    </div>
  )}
/>

renderFooter

story कंटेंट के नीचे footer जोड़ें:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderFooter={({ author, story, storyIndex }) => (
    <div style={{
      position: 'absolute',
      bottom: 0,
      left: 0,
      right: 0,
      padding: 16,
      background: 'linear-gradient(transparent, rgba(0,0,0,0.6))',
      color: '#fff',
      zIndex: 10,
    }}>
      <span>{author.name} — Story {storyIndex + 1}</span>
    </div>
  )}
/>

renderSlide

डिफ़ॉल्ट इमेज/वीडियो स्लाइड पूरी तरह बदलें। बिल्ट-इन मीडिया हैंडलिंग के लिए ImageStorySlide और VideoStorySlide sub-components इस्तेमाल करें:

tsx
import {
  StoriesOverlay,
  ImageStorySlide,
  VideoStorySlide,
} from '@reelkit/react-stories-player';

<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderSlide={({ story, index, groupIndex, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }) => {
    const [w, h] = size;

    if (story.mediaType === 'video') {
      return (
        <div style={{ width: w, height: h, background: '#000' }}>
          <VideoStorySlide
            src={story.src}
            poster={story.poster}
            groupIndex={groupIndex}
            storyIndex={index}
            activeGroupIndex={activeGroupIndex}
            activeStoryIndex={activeStoryIndex}
            onDurationReady={onDurationReady}
            onPlaying={onReady}
            onWaiting={onWaiting}
            onEnded={onEnded}
            onError={onError}
          />
        </div>
      );
    }

    return (
      <div style={{ width: w, height: h, background: '#000' }}>
        <ImageStorySlide src={story.src} onLoad={onReady} onError={onError} />
      </div>
    );
  }}
/>

renderNavigation

डिफ़ॉल्ट डेस्कटॉप chevron बटन बदलें:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderNavigation={({ onPrevStory, onNextStory, onPrevGroup, onNextGroup }) => (
    <div style={{ position: 'absolute', bottom: 16, left: 0, right: 0, display: 'flex', justifyContent: 'center', gap: 8, zIndex: 10 }}>
      <button onClick={onPrevGroup}>Prev Group</button>
      <button onClick={onPrevStory}>Prev</button>
      <button onClick={onNextStory}>Next</button>
      <button onClick={onNextGroup}>Next Group</button>
    </div>
  )}
/>

renderProgressBar

डिफ़ॉल्ट canvas प्रोग्रेस बार की जगह अपना implementation लगाएँ। progress signal 0 से 1 तक के मान देता है:

tsx
import { Observe } from '@reelkit/react';

<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderProgressBar={({ totalStories, activeIndex, progress }) => (
    <div style={{ display: 'flex', gap: 4, padding: '8px 16px' }}>
      {Array.from({ length: totalStories }, (_, i) => (
        <div key={i} style={{ flex: 1, height: 2, background: 'rgba(255,255,255,0.3)', borderRadius: 1, overflow: 'hidden' }}>
          <Observe signals={[activeIndex, progress]}>
            {() => {
              const fill = i < activeIndex.value ? 1 : i === activeIndex.value ? progress.value : 0;
              return <div style={{ width: `${fill * 100}%`, height: '100%', background: '#fff' }} />;
            }}
          </Observe>
        </div>
      ))}
    </div>
  )}
/>

renderLoading

कंटेंट fetch होते समय कस्टम लोडिंग इंडिकेटर:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderLoading={({ story, storyIndex, groupIndex }) => (
    <div style={{
      position: 'absolute',
      inset: 0,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      color: '#fff',
    }}>
      <span>Loading story {storyIndex + 1}...</span>
    </div>
  )}
/>

renderError

कंटेंट लोड न होने पर कस्टम एरर overlay:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderError={({ story, storyIndex, groupIndex }) => (
    <div style={{
      position: 'absolute',
      inset: 0,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      flexDirection: 'column',
      color: '#fff',
      background: 'rgba(0,0,0,0.8)',
    }}>
      <span style={{ fontSize: 48 }}>!</span>
      <span>Failed to load story</span>
    </div>
  )}
/>

renderGroupPreview

डेस्कटॉप carousel कार्ड का content बदलें। कार्ड की जगह, आकार और slide अब भी player संभालता है; group खोलने के लिए onOpen कॉल करें:

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  desktopLayout="carousel"
  renderGroupPreview={({ group, story, onOpen }) => (
    <button
      type="button"
      className="my-story-card"
      aria-label={`Open stories by ${group.author.name}`}
      onClick={onOpen}
    >
      {story?.poster && <img src={story.poster} alt="" />}
      <span>{group.author.name}</span>
    </button>
  )}
/>

StoriesApi

imperative नियंत्रण के लिए apiRef prop इस्तेमाल करें:

tsx
import { useRef } from 'react';
import { StoriesOverlay, type StoriesApi } from '@reelkit/react-stories-player';

function App() {
  const apiRef = useRef<StoriesApi | null>(null);

  return (
    <>
      <button onClick={() => apiRef.current?.nextStory()}>Next Story</button>
      <button onClick={() => apiRef.current?.nextGroup()}>Next Group</button>
      <button onClick={() => apiRef.current?.pause()}>Pause</button>

      <StoriesOverlay
        isOpen={isOpen}
        onClose={handleClose}
        groups={groups}
        apiRef={apiRef}
      />
    </>
  );
}

मेथड

मेथडटाइपविवरण
nextStory()() => voidमौजूदा ग्रुप के अंदर अगली story पर जाता है
prevStory()() => voidमौजूदा ग्रुप के अंदर पिछली story पर जाता है
nextGroup()() => voidअगले यूज़र ग्रुप पर जाता है
prevGroup()() => voidपिछले यूज़र ग्रुप पर जाता है
goToGroup(index)(index: number) => voidindex से किसी खास ग्रुप पर सीधे जाता है
pause()() => voidअपने-आप आगे बढ़ना और प्रोग्रेस टाइमर रोकता है
resume()() => voidअपने-आप आगे बढ़ना और प्रोग्रेस टाइमर फिर शुरू करता है

डबल-टैप और लाइक

डबल-टैप पर एक बिल्ट-इन दिल का एनिमेशन चलता है, जो तुरंत दिखने वाली प्रतिक्रिया देता है। onDoubleTap callback ग्रुप और story index के साथ चलता है, ताकि आप लाइक को अपनी स्टेट में सहेज सकें (API कॉल, local storage वगैरह)। प्लेयर अंदर से लाइक की स्टेट नहीं संभालता।

tsx
<StoriesOverlay
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  groups={groups}
  onDoubleTap={(groupIndex, storyIndex) => {
    // Built-in heart animation plays automatically.
    // Handle the like in your own state:
    const story = groups[groupIndex].stories[storyIndex];
    toggleLike(story.id);
  }}
/>

दिल का एनिमेशन कस्टमाइज़ करना

एनिमेशन की रफ़्तार --rk-stories-heart-duration token से बदलें (थीमिंग देखें)। रंग, आकार या दिल को पूरी तरह छिपाने के लिए सीधे .rk-stories-heart class को निशाना बनाएँ। HeartAnimation कंपोनेंट अलग से इस्तेमाल के लिए भी export होता है।

css
/* Speed up the pop via the token */
:root {
  --rk-stories-heart-duration: 1s;
}

/* Restyle color and size via the class */
.rk-stories-heart {
  color: #ff3b5c;
  font-size: 80px;
}

/* Or hide the built-in heart entirely */
.rk-stories-heart {
  display: none;
}

बिल्ट-इन दिल का एनिमेशन अभी render prop से नहीं बदला जा सकता। आप इसे CSS से नया रूप दे सकते हैं या display: none से छिपाकर onDoubleTap callback में अपना एनिमेशन चला सकते हैं। अगर आपको renderDoubleTap render prop चाहिए, तो GitHub Issues पर बताएँ।

सब-कंपोनेंट

कस्टम render props में जोड़कर इस्तेमाल करने के लिए export किए गए, दोबारा इस्तेमाल होने वाले हिस्से। हर एक का props type भी साथ में export होता है — CanvasProgressBarProps, StoryHeaderProps, ImageStorySlideProps, VideoStorySlideProps, StoriesRingProps, StoriesRingListProps और HeartAnimationProps — किसी को लपेटने वाले component के लिए:

CanvasProgressBar

canvas पर आधारित तेज़ segmented प्रोग्रेस बार। हर story का segment बनाता है और सक्रिय segment का भराव requestAnimationFrame से एनिमेट करता है। कई stories वाले ग्रुप के लिए खिसकती विंडो का सपोर्ट है। जिस बार को बस यह दिखाना है कि ग्रुप कहाँ रुका है, उसे live={false} दें: वह बिना animation loop के सिर्फ़ signals बदलने पर और resize के बाद बनता है।

tsx
import { CanvasProgressBar } from '@reelkit/react-stories-player';

<CanvasProgressBar
  totalStories={group.stories.length}
  activeIndex={activeIndexSignal}
  progress={progressSignal}
  minSegmentWidth={8}
  gap={2}
  barHeight={2}
  // A bar that is not live draws only when its signals change, with no
  // animation loop: enough for a group that is not playing.
  live={isActiveGroup}
/>

StoryHeader

लेखक के avatar, नाम, verified बैज, सापेक्ष समय, pause/play टॉगल, mute/unmute टॉगल, लोडिंग spinner और बंद करने के बटन वाला डिफ़ॉल्ट header। renderHeader न देने पर अपने-आप इस्तेमाल होता है।

tsx
import { StoryHeader } from '@reelkit/react-stories-player';

<StoryHeader
  author={{ id: '1', name: 'Alice', avatar: '/avatar.jpg', verified: true }}
  createdAt={new Date(Date.now() - 3600_000)}
  onClose={handleClose}
  isPaused={false}
  onTogglePause={togglePause}
  isMuted={true}
  onToggleSound={toggleSound}
  isVideo={true}
  isLoading={false}
/>

ImageStorySlide

object-fit: cover वाली, किनारे तक फैली इमेज स्लाइड। lifecycle ट्रैकिंग के लिए callbacks से लोड/एरर बताती है।

tsx
import { ImageStorySlide } from '@reelkit/react-stories-player';

<ImageStorySlide
  src="/photo.jpg"
  aspectRatio={9 / 16}
  onLoad={() => console.log('loaded')}
  onError={() => console.log('failed')}
/>

VideoStorySlide

iOS पर आवाज़ बनाए रखने के लिए साझा <video> एलिमेंट इस्तेमाल करने वाली वीडियो स्लाइड। autoplay, poster फ़्रेम और साउंड sync संभालती है, और अवधि व प्लेबैक lifecycle के इवेंट बताती है।

tsx
import { VideoStorySlide } from '@reelkit/react-stories-player';

<VideoStorySlide
  src="/clip.mp4"
  poster="/clip-poster.jpg"
  groupIndex={0}
  storyIndex={2}
  activeGroupIndex={activeGroupSignal}
  activeStoryIndex={activeStorySignal}
  onDurationReady={(ms) => console.log('duration:', ms)}
  onPlaying={() => console.log('playing')}
  onWaiting={() => console.log('buffering')}
  onEnded={() => console.log('ended')}
  onError={() => console.log('error')}
/>

StoriesRing

Instagram जैसी gradient ring वाला गोल avatar। दो स्थितियाँ: जिस ग्रुप में देखने को stories बाकी हैं उसे घूमता gradient मिलता है, पूरा देखे जा चुके ग्रुप को फीकी सपाट ring।

tsx
import { StoriesRing } from '@reelkit/react-stories-player';

<StoriesRing
  author={{ id: '1', name: 'Alice', avatar: '/avatar.jpg' }}
  totalStories={5}
  viewedCount={2}
  onClick={() => openStories(0)}
/>

StoriesRingList

लेखकों के नाम के साथ StoriesRing कंपोनेंट की हॉरिज़ॉन्टल स्क्रॉल होने वाली पंक्ति। हर ग्रुप की एक ring।

tsx
import { StoriesRingList } from '@reelkit/react-stories-player';

// `viewed` is the same createStoriesViewedStateController the player takes; the
// rings follow it by themselves.
<StoriesRingList
  groups={groups}
  viewed={viewed}
  onSelect={(groupIndex) => openStories(groupIndex)}
/>;

HeartAnimation

डबल-टैप पर चलने वाला एनिमेटेड दिल का overlay। 800ms में बड़ा होकर धुंधला होता हुआ गायब हो जाता है। CSS से कस्टमाइज़ करें (डबल-टैप और लाइक वाला हिस्सा देखें)।

tsx
import { HeartAnimation } from '@reelkit/react-stories-player';

<HeartAnimation onComplete={() => console.log('animation done')} />

फिर से एक्सपोर्ट

उपभोक्ता को जो कुछ चाहिए, वह सब इसी पैकेज से मिलता है, इसलिए @reelkit/core से सीधा import कभी ज़रूरी नहीं होता।

कहाँ सेफिर से एक्सपोर्ट
@reelkit/reactSoundProvider, useSoundState, useOverlayUrlState, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, और टाइप UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, OverlayUrlStateOptions, ViewedStateController, ViewedStateOptions, StorageAdapter
@reelkit/stories-corecreateStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, और कंटेंट टाइप StoryItem, AuthorInfo, StoriesGroup, MediaType

SoundProvider और useSoundState वह sound स्टेट रखते हैं जो player अपनी स्लाइडों के साथ साझा करता है। अपना media चलाने वाली कस्टम स्लाइड mute toggle को useSoundState से पढ़ती है। Player के अंदर provider पहले से मौजूद है; कहीं और render की गई VideoStorySlide या कस्टम स्लाइड के ऊपर SoundProvider होना चाहिए, वरना useSoundState एरर throw करता है।

टाइप

StoryItem

typescript
interface StoryItem {
  id: string;
  mediaType: 'image' | 'video';
  src: string;
  poster?: string;
  duration?: number;       // ms, images default to 5000, videos use natural duration
  createdAt?: string | Date;
  aspectRatio?: number;    // width / height
}

AuthorInfo

typescript
interface AuthorInfo {
  id: string;
  name: string;
  avatar: string;
  verified?: boolean;
}

StoriesGroup<T>

typescript
interface StoriesGroup<T extends StoryItem = StoryItem> {
  author: AuthorInfo;
  stories: T[];
}

HeaderRenderProps<T>

typescript
interface HeaderRenderProps<T extends StoryItem = StoryItem> {
  author: AuthorInfo;
  story: T;
  storyIndex: number;
  isPaused: boolean;
  isMuted: boolean;
  isVideo: boolean;
  groupIndex: number;
  isActive: boolean; // false for a neighbouring group's header
  onToggleSound: () => void;
  onTogglePause: () => void;
  onClose: () => void;
}

FooterRenderProps<T>

typescript
interface FooterRenderProps<T extends StoryItem = StoryItem> {
  author: AuthorInfo;
  story: T;
  storyIndex: number;
}

SlideRenderProps<T>

typescript
interface SlideRenderProps<T extends StoryItem = StoryItem> {
  story: T;
  index: number;
  groupIndex: number;
  isActive: boolean;
  size: [number, number];
  activeGroupIndex: Signal<number>;
  activeStoryIndex: Signal<number>;
  onDurationReady: (durationMs: number) => void;
  onReady: () => void;
  onWaiting: () => void;
  onError: () => void;
  onEnded: () => void;
}
typescript
interface NavigationRenderProps {
  onPrevStory: () => void;
  onNextStory: () => void;
  onPrevGroup: () => void;
  onNextGroup: () => void;
}

ProgressBarRenderProps<T>

typescript
interface ProgressBarRenderProps<T extends StoryItem = StoryItem> {
  totalStories: number;
  activeIndex: Signal<number>;
  progress: Signal<number>;
  group: StoriesGroup<T>;
  groupIndex: number;
  isActive: boolean; // false: the signals hold still for a neighbouring group
}

LoadingRenderProps<T>

typescript
interface LoadingRenderProps<T extends StoryItem = StoryItem> {
  story: T;
  storyIndex: number;
  groupIndex: number;
}

ErrorRenderProps<T>

typescript
interface ErrorRenderProps<T extends StoryItem = StoryItem> {
  story: T;
  storyIndex: number;
  groupIndex: number;
}

GroupPreviewRenderProps<T>

typescript
type DesktopLayout = 'single' | 'carousel';

interface GroupPreviewRenderProps<T extends StoryItem = StoryItem> {
  group: StoriesGroup<T>;
  groupIndex: number;
  story: T | undefined; // the story the group would open on
  offset: number; // negative on the left, positive on the right
  viewedCount: number; // from viewedState, 0 without it
  onOpen: () => void;
}

StoriesApi

typescript
interface StoriesApi {
  nextStory: () => void;
  prevStory: () => void;
  nextGroup: () => void;
  prevGroup: () => void;
  goToGroup: (index: number) => void;
  pause: () => void;
  resume: () => void;
}

कस्टम story टाइप

StoryItem को कस्टम fields से extend करें और टाइप parameter StoriesOverlay को पास करें। सारे render props को आपका extend किया गया टाइप मिलेगा:

tsx
import {
  StoriesOverlay,
  ImageStorySlide,
  type StoryItem,
  type StoriesGroup,
  type SlideRenderProps,
} from '@reelkit/react-stories-player';

interface PromoStory extends StoryItem {
  title: string;
  subtitle?: string;
  bgGradient?: string;
  ctaText?: string;
}

const groups: StoriesGroup<PromoStory>[] = [
  {
    author: { id: 'brand', name: 'My Brand', avatar: '/brand.png', verified: true },
    stories: [
      {
        id: 'promo-1',
        mediaType: 'image',
        src: '/sale-banner.jpg',
        title: 'Flash Sale',
        subtitle: 'Up to 50% off',
        ctaText: 'Shop Now',
      },
      {
        id: 'promo-2',
        mediaType: 'image',
        src: '',
        title: 'Thank You',
        subtitle: '10K followers!',
        bgGradient: 'linear-gradient(135deg, #a18cd1, #fbc2eb)',
      },
    ],
  },
];

function CustomSlide({ story, size, onReady, onError }: SlideRenderProps<PromoStory>) {
  const [w, h] = size;
  const hasImage = story.src && story.mediaType === 'image';

  return (
    <div style={{ width: w, height: h, background: story.bgGradient ?? '#000', position: 'relative' }}>
      {hasImage && <ImageStorySlide src={story.src} onLoad={onReady} onError={onError} />}
      <div style={{ position: 'relative', zIndex: 1, textAlign: 'center', padding: 32, color: '#fff' }}>
        <h2>{story.title}</h2>
        {story.subtitle && <p>{story.subtitle}</p>}
        {story.ctaText && (
          <button style={{ marginTop: 16, padding: '8px 24px', borderRadius: 20, background: '#fff', color: '#000', border: 'none' }}>
            {story.ctaText}
          </button>
        )}
      </div>
    </div>
  );
}

<StoriesOverlay<PromoStory>
  isOpen={isOpen}
  onClose={handleClose}
  groups={groups}
  renderSlide={(props) => <CustomSlide {...props} />}
/>

CSS क्लास

सारी CSS क्लास साधारण हैं (CSS modules नहीं), इसलिए @reelkit/react-stories-player/styles.css के बाद लोड होने वाली stylesheet में ज़्यादा specificity वाले selectors से इन्हें निशाना बनाया जा सकता है। रंग, आकार और z-index बदलने के लिए नीचे थीमिंग हिस्से में बताई CSS custom properties को प्राथमिकता दें।

क्लासकंपोनेंटविवरण
.rk-stories-overlayOverlayतय फ़ुलस्क्रीन backdrop (background, z-index)
.rk-stories-swipe-wrapperOverlayस्वाइप करके बंद करने वाला wrapper (नेविगेशन बटन + canvas रखता है)
.rk-stories-containerOverlayगोल कोनों वाला story canvas (position, overflow)
.rk-stories-ui-layerOverlayUI overlay का कंटेनर (header, प्रोग्रेस, नेविगेशन)। chromePlacement="group" के साथ हर ग्रुप की slide के अंदर
.rk-stories-ui-layer--hiddenOverlayUI की छिपी स्टेट (hideUIOnPause से बदलती है)
.rk-stories-errorOverlayएरर स्टेट (बीच में आइकन + टेक्स्ट)
.rk-stories-error-textOverlayएरर संदेश का टेक्स्ट
.rk-stories-nav-btnNavigationडेस्कटॉप का prev/next तीर
.rk-stories-progress-barProgressBarcanvas प्रोग्रेस बार की स्थिति वाला wrapper
.rk-stories-slide-wrapperGroupstories का एक ग्रुप (बाहरी स्लाइड)
.rk-stories-storyStoryएक अकेली story (अंदरूनी स्लाइड का root)
.rk-stories-headerStoryHeaderHeader bar (avatar, नाम, actions)
.rk-stories-header--hiddenStoryHeaderHeader की छिपी स्टेट (visible=false)
.rk-stories-header-avatarStoryHeaderलेखक की avatar इमेज
.rk-stories-header-nameStoryHeaderलेखक के नाम का टेक्स्ट
.rk-stories-header-verifiedStoryHeaderVerified बैज का कंटेनर
.rk-stories-header-timeStoryHeaderकितनी देर पहले का टेक्स्ट
.rk-stories-header-actionsStoryHeaderदाईं ओर के actions (बंद, mute, pause)
.rk-stories-header-btnStoryHeaderHeader का action बटन
.rk-stories-header-btn--desktopStoryHeaderPause बटन, सिर्फ़ 768px से चौड़ी स्क्रीन पर दिखता है
.rk-stories-header-spinnerStoryHeaderवीडियो buffer होने का spinner
.rk-stories-imageImageStorySlideइमेज story एलिमेंट
.rk-stories-videoVideoStorySlideवीडियो story कंटेनर
.rk-stories-video-elementVideoStorySlideसाझा <video> एलिमेंट
.rk-stories-video-posterVideoStorySlideवीडियो की poster इमेज (चलने पर धुंधली होकर गायब)
.rk-stories-video-poster--visibleVideoStorySlidePoster की दिखने वाली स्टेट (प्लेबैक से पहले)
.rk-stories-heartHeartAnimationडबल-टैप पर दिल उभरने का एनिमेशन
.rk-stories-ringStoriesRingStory ring (एनिमेटेड gradient border वाला avatar)
.rk-stories-ring--activeStoriesRingदेखने को stories बाकी होने वाली ring (एनिमेट होती है)
.rk-stories-ring-avatarStoriesRingring के अंदर की avatar इमेज
.rk-stories-ring-listStoriesRingListहॉरिज़ॉन्टल ring सूची का कंटेनर
.rk-stories-ring-list-itemStoriesRingListRing + नाम वाला कॉलम
.rk-stories-ring-list-nameStoriesRingListहर ring के नीचे लेखक का नाम
.rk-stories-overlay--carouselOverlayडेस्कटॉप carousel दिखाता हुआ overlay
.rk-stories-overlay--slidingOverlaycarousel slide चल रही है; player canvas छिपा है
.rk-stories-carouselCarouselplayer के पीछे preview कार्ड की layer
.rk-stories-carousel--instantCarouselslide का पहला frame, बिना transition के रखा गया
.rk-stories-cardCarouselएक positioned preview कार्ड
.rk-stories-card--centerCarouselslide के दौरान बीच वाली जगह पर कार्ड
.rk-stories-card--hiddenCarouselएक तरफ़ दिखने वाले दो कार्ड से आगे का कार्ड
.rk-stories-card-buttonCarouselडिफ़ॉल्ट कार्ड content (group खोलने वाला बटन)
.rk-stories-card-imageCarouselpreview frame (poster या इमेज)
.rk-stories-card-frameCarouselबिना इमेज या poster वाली story का छोटा किया गया renderSlide preview; निष्क्रिय
.rk-stories-card-scrimCarouselframe के ऊपर धुंधली layer
.rk-stories-card-infoCarouselring, author का नाम और समय वाला column
.rk-stories-card-nameCarouselauthor का नाम
.rk-stories-card-timeCarouselकितने समय पहले वाला टेक्स्ट

थीमिंग

हर रंग, आकार, z-index और ट्रांज़िशन एक CSS custom property में है। कंपोनेंट का source छुए बिना थीम बदलने के लिए :root (या overlay के किसी भी ancestor) पर एक या कई override करें।

Tokenडिफ़ॉल्टक्या नियंत्रित करता है
--rk-stories-overlay-bg#000फ़ुलस्क्रीन backdrop का रंग
--rk-stories-overlay-z9999Overlay का z-index
--rk-stories-swipe-gap16pxनेविगेशन बटन और story canvas के बीच की दूरी
--rk-stories-container-radius12pxstory canvas के गोल कोने (डेस्कटॉप)
--rk-stories-container-radius-mobile0768px चौड़ाई तक story canvas के कोने
--rk-stories-ui-z15UI layer (header, प्रोग्रेस बार) का z-index
--rk-stories-ui-transition200mshideUIOnPause बदलने पर धुंधला होने की अवधि
--rk-stories-top-shade-height120pxheader के पीछे ऊपर के gradient की ऊँचाई
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)ऊपर के gradient का रंग
--rk-stories-error-z5एरर स्टेट का z-index
--rk-stories-error-bglinear-gradient(145deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)एरर स्टेट का background gradient
--rk-stories-error-fgrgba(255, 255, 255, 0.5)एरर आइकन और टेक्स्ट का रंग
--rk-stories-error-gap12pxएरर आइकन और उसके टेक्स्ट के बीच की दूरी
--rk-stories-error-text-size13pxएरर संदेश का font size
--rk-stories-nav-z20डेस्कटॉप नेविगेशन बटन का z-index
--rk-stories-nav-size44pxडेस्कटॉप prev/next बटन का आकार
--rk-stories-nav-bgrgba(255, 255, 255, 0.1)डेस्कटॉप नेविगेशन बटन का background
--rk-stories-nav-bg-hoverrgba(255, 255, 255, 0.2)डेस्कटॉप नेविगेशन बटन का hover background
--rk-stories-nav-fgrgba(255, 255, 255, 0.7)डेस्कटॉप नेविगेशन बटन के आइकन का रंग
--rk-stories-nav-fg-hover#fffडेस्कटॉप नेविगेशन बटन के आइकन का hover रंग
--rk-stories-nav-transition150msनेविगेशन बटन के background और रंग का ट्रांज़िशन
--rk-stories-video-bg#000<video> के पीछे letterbox background
--rk-stories-video-poster-transition200msवीडियो चलना शुरू होने पर poster के धुंधला होने की अवधि
--rk-stories-progress-bar-z10प्रोग्रेस बार का z-index
--rk-stories-progress-bar-padding8px 8px 0प्रोग्रेस बार के चारों ओर की जगह
--rk-stories-header-z5Header का z-index
--rk-stories-header-top18pxstory के ऊपर से header की वर्टिकल दूरी
--rk-stories-header-padding12px 16pxheader पंक्ति की अंदरूनी padding
--rk-stories-header-gap8pxavatar, नाम और समय के बीच की दूरी
--rk-stories-header-transition200msछिपने पर header के धुंधला होने की अवधि
--rk-stories-header-avatar-size32pxAvatar की चौड़ाई/ऊँचाई
--rk-stories-header-name-fg#fffलेखक के नाम का रंग
--rk-stories-header-name-size14pxलेखक के नाम का font size
--rk-stories-header-name-weight600लेखक के नाम का font weight
--rk-stories-header-time-fgrgba(255, 255, 255, 0.6)कितनी देर पहले वाले टेक्स्ट का रंग
--rk-stories-header-time-size12pxकितनी देर पहले वाले टेक्स्ट का font size
--rk-stories-header-actions-gap8pxHeader के action बटनों के बीच की दूरी
--rk-stories-header-btn-fg#fffHeader के action आइकन का रंग (बंद, mute, pause)
--rk-stories-header-btn-padding4pxHeader के action बटन की padding
--rk-stories-header-spinner-size20pxbuffering spinner का आकार
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)buffering spinner के track का रंग
--rk-stories-header-spinner-fg#fffbuffering spinner के चाप का रंग
--rk-stories-header-spinner-duration0.8sbuffering spinner का एक पूरा चक्कर
--rk-stories-heart-z20डबल-टैप वाले दिल का z-index
--rk-stories-heart-duration800msउभरने/धुंधला होने के एनिमेशन की अवधि
--rk-stories-ring-spin-duration4sदेखने को stories बाकी होने वाली ring के घूमने की अवधि
--rk-stories-ring-active-scale0.95दबाए जाने के दौरान ring का scale
--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 न दिया जाए
--rk-stories-ring-list-gap12pxसूची में rings के बीच की दूरी
--rk-stories-ring-list-padding12pxring सूची के चारों ओर की अंदरूनी padding
--rk-stories-ring-list-item-gap4pxring और उसके नाम के बीच की दूरी
--rk-stories-ring-list-name-size12pxहर ring के नीचे लेखक के नाम का font size
--rk-stories-card-bg#262626carousel कार्ड का background, preview frame न होने पर दिखता है
--rk-stories-card-radius8pxcarousel कार्ड के कोनों की गोलाई
--rk-stories-card-scrimrgba(0, 0, 0, 0.45)बगल वाले कार्ड पर धुंधलापन
--rk-stories-card-fg#fffकार्ड के टेक्स्ट का रंग
--rk-stories-card-name-size14pxकार्ड पर author के नाम का font size
--rk-stories-card-time-fgrgba(255, 255, 255, 0.7)कार्ड पर समय वाले टेक्स्ट का रंग
--rk-stories-card-time-size13pxकार्ड पर समय वाले टेक्स्ट का font size
--rk-stories-card-gap6pxकार्ड पर ring, नाम और समय के बीच की दूरी
--rk-stories-card-transition300mscarousel slide की अवधि

नीचे का snippet @reelkit/react-stories-player/styles.css के बाद लोड होने वाली stylesheet में डालें।

css
/* Brand the stories overlay */
:root {
  --rk-stories-overlay-bg: #0f172a;
  --rk-stories-container-radius: 24px;
  --rk-stories-nav-bg: rgba(99, 102, 241, 0.25);
  --rk-stories-nav-bg-hover: rgba(168, 85, 247, 0.55);
  --rk-stories-top-shade-bg: linear-gradient(
    to bottom,
    rgba(99, 102, 241, 0.5) 0%,
    transparent 100%
  );
  --rk-stories-header-name-fg: #fef3c7;
  --rk-stories-ring-spin-duration: 2s;
}

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

सुलभता

Overlay का root एक modal dialog है (role="dialog", aria-modal="true")। स्क्रीन रीडर की घोषणा बदलने के लिए ariaLabel सेट करें; डिफ़ॉल्ट "Stories player" है।

Overlay खुलने पर फ़ोकस पकड़ता है और बंद होने पर उसे trigger पर लौटा देता है। Tab और Shift+Tab अंदर के फ़ोकस होने वाले एलिमेंट में घूमते हैं; बाहर निकला फ़ोकस (बाहर क्लिक, प्रोग्राम से फ़ोकस) वापस खींच लिया जाता है। यह @reelkit/core के captureFocusForReturn और createFocusTrap से बना है, जिन्हें @reelkit/react फिर से एक्सपोर्ट करता है।

डेस्कटॉप carousel में हर डिफ़ॉल्ट कार्ड एक बटन है, जिसका label "Open stories by" और author का नाम है। Tab क्रम में कार्ड player के controls के बाद आते हैं, और slide चलने के दौरान उस क्रम से बाहर रहते हैं।

सर्वर-साइड रेंडरिंग

बंद रहते हुए overlay कुछ भी render नहीं करता, और document.body में portal सिर्फ़ खुलने पर करता है, जो client पर होता है। StoriesUrlOverlay address bar को mount के बाद एक effect में पढ़ता है, इसलिए server render इसे कभी नहीं खोलता। StoriesRingList server पर हर ring को बिना देखी हुई render करता है: viewed store सिर्फ़ mount के बाद पढ़ा जाता है, इसलिए server का markup और client का पहला render एक जैसे रहते हैं। पूरी तस्वीर के लिए SSR गाइड देखें।

कीबोर्ड शॉर्टकट

कुंजीक्रिया
ArrowLeftपिछली story। ग्रुप की पहली story पर, पिछला ग्रुप, उसी story पर जहाँ उसे छोड़ा गया था (वरना उसकी resume story पर); पहले ग्रुप पर कुछ नहीं
ArrowRightअगली story। ग्रुप की आख़िरी story के बाद, अगला ग्रुप; आख़िरी ग्रुप के बाद player बंद हो जाता है
Escapeप्लेयर बंद करें