Vue Stories Player

@reelkit/vue-stories-player पर बना, Vue 3 के लिए Instagram जैसा Stories Player ओवरले। यह React वाले प्लेयर का ही @reelkit/stories-core इंजन इस्तेमाल करता है, इसलिए व्यवहार, CSS क्लास और थीमिंग टोकन एक जैसे हैं।

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

Features

नेस्टेड नेविगेशन
टैप से स्टोरी, स्वाइप से उपयोगकर्ता
वीडियो स्टोरीज़
ऑटोप्ले के साथ साउंड टॉगल
ऑटो-एडवांस
हर स्टोरी के लिए टाइमर सेट करें
3D ट्रांज़िशन
क्यूब, फ्लिप, फ़ेड, ज़ूम, स्लाइड
प्रोग्रेस बार
canvas पर बनी खंडों वाली बार
इमेज और वीडियो
दोनों तरह का मीडिया
वर्चुअलाइज़्ड
DOM में सिर्फ़ 3 स्लाइड
डबल-टैप लाइक
डबल-टैप पर दिल का एनिमेशन
डेस्कटॉप नेविगेशन
तीर वाले बटन
स्टोरी रिंग
Instagram जैसे अवतार रिंग
जेनेरिक टाइप
StoryItem को अपने फ़ील्ड से बढ़ाएँ
स्कोप्ड स्लॉट
इंटरफ़ेस का कोई भी हिस्सा बदलें
URL स्थिति
साझा करने योग्य ?story=group.story लिंक
देखी गई स्थिति
रिंग और जहाँ छोड़ा था, रीलोड के बाद भी बने रहते हैं

इंस्टॉलेशन

bash
npm install @reelkit/vue-stories-player @reelkit/vue lucide-vue-next

स्टाइलशीट को ऐप के एंट्री पॉइंट (या किसी भी कंपोनेंट) में एक बार इंपोर्ट करें:

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

डिफ़ॉल्ट हेडर और नेविगेशन तीर lucide-vue-next के आइकन इस्तेमाल करते हैं। अगर आप कोई और आइकन लाइब्रेरी पसंद करते हैं, तो #header और #navigation स्कोप्ड स्लॉट से उन्हें बदल दें।

Vue 3.3 या नया

ओवरले जेनेरिक कंपोनेंट हैं, जिन्हें Vue 3.3 से सपोर्ट करता है। पुराने Vue पर runtime उनके props और events छोड़ देता है, इसलिए यह पैकेज peer dependency के रूप में vue 3.3 या नया माँगता है।

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

StoriesOverlay पूरा स्क्रीन घेरने वाला प्लेयर दिखाता है। Instagram जैसे एंट्री पॉइंट के लिए इसे StoriesRingList के साथ रखें, StoriesGroup ऑब्जेक्ट की सूची दें और v-model:is-open बाइंड करें।

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  StoriesOverlay,
  StoriesRingList,
  type StoriesGroup,
} from '@reelkit/vue-stories-player';
import '@reelkit/vue-stories-player/styles.css';

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

const isOpen = ref(false);
const groupIndex = ref(0);

const openGroup = (index: number) => {
  groupIndex.value = index;
  isOpen.value = true;
};
</script>

<template>
  <StoriesRingList :groups="groups" @select="openGroup" />

  <!-- v-model:is-open keeps the ref in sync however the viewer closes it:
       the ✕ button, a swipe down, Escape, or the last story finishing. -->
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="groups"
    :initial-group-index="groupIndex"
  />
</template>

URL स्थिति

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

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

तैयार कीज़

स्टोरीज़ दो-अक्ष वाली हैं, इसलिए कंट्रोलर में दो-अक्ष वाली key फैलाएँ: urlIndexTwoAxisKey (उपयोगकर्ता और स्टोरी स्थिति से) या urlStableIdTwoAxisKey (उपयोगकर्ता स्थायी id से) — दोनों @reelkit/vue-stories-player से फिर से एक्सपोर्ट होती हैं। URL स्थिति गाइड और Core API देखें।

vue
<script setup lang="ts">
import {
  StoriesUrlOverlay,
  useOverlayUrlState,
  urlIndexTwoAxisKey,
  type StoriesGroup,
} from '@reelkit/vue-stories-player';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-stories-player/styles.css';

const props = defineProps<{ groups: StoriesGroup[] }>();

// The outer axis is the group, the inner one the story inside it.
const stories = useOverlayUrlState({
  param: 'story',
  adapter: useVueRouterUrlAdapter(),
  ...urlIndexTwoAxisKey({
    outerCount: () => props.groups.length,
    innerCounts: () => props.groups.map((group) => group.stories.length),
  }),
});
</script>

<template>
  <!-- Opening is a link — the overlay reads the URL and opens itself. -->
  <RouterLink
    v-for="(group, index) in props.groups"
    :key="group.author.id"
    :to="`?story=${index}.0`"
  >
    {{ group.author.name }}
  </RouterLink>

  <StoriesUrlOverlay :controller="stories" :groups="props.groups" />
</template>
  • खुलने पर हिस्ट्री में एक एंट्री जुड़ती है। स्टोरीज़ के बीच बढ़ना और उपयोगकर्ता बदलना, दोनों उसी एंट्री को बदलते हैं, इसलिए कितनी भी नेविगेशन से एंट्री नहीं बढ़तीं और एक बार बैक दबाने पर प्लेयर हमेशा बंद हो जाता है। बैक बंद करता है, पिछली स्टोरी पर नहीं ले जाता।
  • अंदरूनी नेविगेशन भी URL में जाती है। एक ही उपयोगकर्ता की स्टोरीज़ में आगे बढ़ने पर ?story=2.n अपडेट होता है, इसलिए डीप लिंक ठीक उसी स्टोरी पर पहुँचता है।
  • बैक तभी बंद करता है जब प्लेयर ऐप के भीतर से खोला गया हो — तब उस लिंक ने एक एंट्री जोड़ी थी। नए टैब में सीधे खोले गए साझा लिंक के पीछे कोई हिस्ट्री नहीं होती, इसलिए ब्राउज़र का बैक साइट से बाहर ले जाता है; ✕ बटन या Escape पैरामीटर को वहीं हटा देते हैं और पेज पर बनाए रखते हैं।
  • ऐसा पैरामीटर जो किसी उपयोगकर्ता या स्टोरी को नहीं बताता — पुराना बुकमार्क, हाथ से बदला मान, समूह के अंत से आगे की स्टोरी — पड़ोसी को खोलने के बजाय URL से हटा दिया जाता है।
  • राउटर वाले ऐप को राउटर पर आधारित अडैप्टर देना चाहिए, ताकि नेविगेशन का एकमात्र स्रोत राउटर ही रहे। @reelkit/vue/vue-router-url-adapter का useVueRouterUrlAdapter Vue Router के लिए तैयार अडैप्टर है; राउटर के पीछे से हिस्ट्री लिखने पर उसकी location पुरानी रह जाती है और अगली नेविगेशन पर पैरामीटर गिर जाता है।

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

typescript
// `?story=alice.0` — the group by the author's stable id, the story by its
// position inside whichever group that id resolves to.
const stories = useOverlayUrlState({
  param: 'story',
  adapter: useVueRouterUrlAdapter(),
  ...urlIndexTwoAxisKey({
    outerCount: () => groups.value.length,
    innerCounts: () => groups.value.map((group) => group.stories.length),
    outerCodec: { decode: (raw) => raw, encode: (id) => id },
    outerLocator: {
      locate: (id) =>
        groups.value.findIndex((group) => group.author.id === id),
      identify: (index) => groups.value[index].author.id,
    },
  }),
});

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

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

यह वही locateAsync पेजर है जो एक-अक्ष वाली कीज़ लेती हैं — दो-अक्ष वाली key में यह आपके दिए outerLocator पर चलता है, इसलिए पेजिंग उपयोगकर्ताओं वाले अक्ष पर होती है और स्टोरी हल हुए समूह के भीतर स्थानीय इंडेक्स बनी रहती है।

typescript
// A feed that pages. `locate` answers for the groups already loaded;
// `locateAsync` runs only when it misses, so a shared link to a group past
// the window fetches up to it and then opens.
outerLocator: {
  locate: (index) => (index < loaded.value.length ? index : null),
  identify: (index) => index,
  locateAsync: async (index) => {
    if (index >= totalGroups) return null;
    loaded.value = await fetchGroupsUpTo(index);
    return index;
  },
},
  • जब तक locateAsync चल रहा है, प्लेयर बंद रहता है और पैरामीटर वैसा का वैसा, इसलिए डीप लिंक फ़ेच के बाद भी बचा रहता है। null या अस्वीकृति पैरामीटर हटा देती है।
  • URL बदल जाने के बाद, बंद हो जाने के बाद या अनमाउंट के बाद आया जवाब छोड़ दिया जाता है — धीमा फ़ेच ऐसी स्टोरी नहीं खोल सकता जिसकी अब किसी ने माँग नहीं की।
  • useOverlayUrlState के सभी विकल्प Vue API संदर्भ में हैं।

जो देखा जा चुका है उसे याद रखना

जब तक किसी उपयोगकर्ता की सारी स्टोरीज़ न देख ली जाएँ, रिंग ग्रेडिएंट दिखाता है, और समूह उस पहली स्टोरी से खुलता है जो दर्शक ने नहीं देखी। ये दोनों एक ही createStoriesViewedStateController से आते हैं: इसे वहीं एक बार बनाएँ जहाँ फ़ीड है और रिंग सूची तथा प्लेयर को viewed के रूप में दें — वे खुद स्टोरेज पढ़ते हैं, उसका अनुसरण करते हैं और जो दिखाया गया उसे दर्ज करते हैं। सहेजी गई प्रविष्टि लेखक और स्टोरी को id से पहचानती है, इसलिए फ़ीड का क्रम बदलने पर भी टिकती है, और वही कंट्रोलर React तथा Angular में भी काम करता है।

vue
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import {
  StoriesOverlay,
  StoriesRingList,
  createStoriesViewedStateController,
  type StoriesGroup,
} from '@reelkit/vue-stories-player';

const props = defineProps<{ groups: StoriesGroup[] }>();

const isOpen = ref(false);
const groupIndex = shallowRef(0);

// One controller for the rings and the player. It reads the groups through
// the getter every time it needs them, so a feed that pages in more groups
// is counted too.
const viewed = createStoriesViewedStateController({
  storageKey: 'stories-seen',
  groups: () => props.groups,
});

const openGroup = (index: number) => {
  groupIndex.value = index;
  isOpen.value = true;
};
</script>

<template>
  <!-- The rings follow the controller by themselves. -->
  <StoriesRingList
    :groups="props.groups"
    :viewed="viewed"
    @select="openGroup"
  />

  <!-- The player resumes, records, and draws its card rings from it. -->
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="props.groups"
    :initial-group-index="groupIndex"
    :viewed="viewed"
  />
</template>
  • जिस स्टोरी से खुला, वह भी गिनी जाती है। स्क्रीन पर पहले से मौजूद स्टोरी को प्लेयर के माउंट होते ही देखी हुई दर्ज कर लिया जाता है, इसलिए एक ही स्टोरी वाला समूह खोलकर बंद करना भी उसे देखा हुआ मान लेता है।
  • आगे बढ़ने पर भी जगह याद रहती है। इस सत्र में पहली बार पहुँचे हर समूह के लिए resumeStoryIndex पूछा जाता है, उस समूह के लिए भी जिससे प्लेयर खुला — बशर्ते initialStoryIndex ने सीधे कोई स्टोरी न बताई हो; जिस समूह को पहले ही देखा जा चुका है वह वहीं से खुलता है जहाँ छोड़ा गया था।
  • लिंक फिर भी भारी पड़ता है। StoriesUrlOverlay में, चाहे जो सहेजा हो, प्लेयर कहाँ खुलेगा यह पैरामीटर तय करता है। बाकी हर जगह यह फ़ैसला रिज़्यूम कॉलबैक करता है।
  • गिनती स्थिति है, जोड़ नहीं। प्रविष्टि सबसे दूर तक पहुँची स्टोरी बताती है, इसलिए देखे जा चुके समूह में नई स्टोरी जोड़ने पर उसका रिंग फिर जल उठता है, और बीच से एक हटाने पर गिनती छोटी हो जाती है।
  • स्टोरेज बदला जा सकता है। टैब बंद होते ही भूलने के लिए storage: createSessionStorageAdapter() दें, या अपना StorageAdapter। दो खुले टैब ब्राउज़र की storage इवेंट से एक साथ चलते रहते हैं।
  • सिर्फ़ खुलने पर माउंट होने वाला प्लेयर बहुत देर से पढ़ता है। ओवरले माउंट होने के बाद, बंद अवस्था में ही स्टोरेज पढ़ता है। v-if="open" में वह क्षण आता ही नहीं, इसलिए controller रखने वाले component में useAttachViewedState(() => viewed) बुलाएँ; यह माउंट होने के बाद जुड़ता है और अनमाउंट पर छोड़ देता है। विकल्प और वे निचले स्तर के हिस्से जिनसे यह बना है, Stories Core पेज पर हैं।

desktop-layout="carousel" सेट करने पर प्लेयर Instagram के वेब व्यूअर जैसा दिखता है: चल रही स्टोरी अपने सामान्य आकार में बीच में रहती है और दोनों ओर अधिकतम दो पड़ोसी समूह छोटे, धुँधले कार्ड की तरह बैठते हैं। कार्ड पर क्लिक करने से वह उपयोगकर्ता खुलता है, और समूह बदलते समय कार्ड सरकते हैं। वही viewed कंट्रोलर दें जो रिंग सूची लेती है — तब देखे जा चुके समूह के कार्ड को धीमा रिंग मिलेगा। कार्ड खुद उसका अनुसरण करते हैं, इसलिए कोई स्टोरी देखी हुई दर्ज होने पर सिर्फ़ वही दोबारा बनते हैं और प्लेयर फिर से रेंडर नहीं होता।

vue
<template>
  <!-- Neighbouring groups sit beside the active story as dimmed cards on a
       desktop screen. Phones keep the plain player whatever this says. -->
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="groups"
    :viewed="viewed"
    desktop-layout="carousel"
  />
</template>
  • फ़ोन पर सादा प्लेयर ही रहता है। 768px चौड़ाई तक कार्ड नहीं बनते और स्वाइप डिफ़ॉल्ट 'single' लेआउट की तरह group-transition का इस्तेमाल करते हैं। उस चौड़ाई के आर-पार आकार बदलने पर लेआउट तुरंत बदल जाता है।
  • हर कार्ड दिखाता है कि समूह कहाँ से खुलेगा: इस सत्र में जहाँ छोड़ा था वह स्टोरी, वरना वही जो resumeStoryIndex लौटाता है। वह वीडियो का पोस्टर या इमेज दिखाता है। जिस स्टोरी के पास दोनों में से कुछ नहीं (जैसे ग्रेडिएंट पर टेक्स्ट वाली स्टोरी), उसे आपका #slide स्लॉट प्लेयर के आकार में बनाता है और फिर छोटा कर दिया जाता है; बिना पोस्टर वाला वीडियो कार्ड की पृष्ठभूमि दिखाता है। इस झलक को isActive: false मिलता है और वह केवल एक तस्वीर है: उसमें न फ़ोकस जाता है, न क्लिक। स्लाइड माउंट होते ही जो भी करती है — प्लेबैक या एनालिटिक्स — उसे isActive की जाँच के पीछे रखें।
  • टाइमर सरकने का इंतज़ार करता है। जब तक कार्ड चलना बंद नहीं करते, स्टोरी आगे नहीं बढ़ती और उसके लिए story-viewed भी नहीं चलता।
  • समूह बदलने का हर तरीक़ा सरकाता है: कार्ड पर क्लिक, StoriesApi.goToGroup, nextGroup और prevGroup, तथा समूह के अंत के आगे तीर कुंजियाँ। छूकर किया गया स्वाइप प्लेयर को खुद ही हिला देता है, इसलिए उसके ऊपर सरकना नहीं जुड़ता।

हर समूह का अपना प्रोग्रेस बार और हेडर

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

vue
<template>
  <!-- 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
    v-model:is-open="isOpen"
    :groups="groups"
    chrome-placement="group"
  />
</template>
  • पड़ोसी समूह दिखाता है कि वह कहाँ रुका है। जब प्लेयर उसकी ओर घूमता है, उसका बार और हेडर वह स्टोरी दिखाते हैं जिस पर समूह खुलेगा, जिसका अभी कुछ भी नहीं चला है।
  • छोड़ा जा रहा समूह जैसा था वैसा रहता है। घुमाव ख़त्म होने तक उसका बार अपनी प्रोग्रेस बनाए रखता है, इसलिए अंत तक देखा गया समूह घुमाव के बीच ख़ाली नहीं होता, भरा ही रहता है।
  • ये स्लॉट स्क्रीन पर मौजूद हर समूह के लिए बनते हैं। #progressBar और #header के स्कोप में groupIndex और isActive होते हैं; पड़ोसी समूह के लिए isActive false होता है और प्रोग्रेस के सिग्नल नहीं बदलते।
  • #header स्लॉट swipe क्षेत्र के अंदर होता है। उस पर tap करने से स्टोरी बदलती है, जब तक tap किसी button, लिंक या role="button" वाले element पर न पड़े, इसलिए उसके हर control को इन्हीं में से एक बनाएँ।
  • डेस्कटॉप कैरोसेल slide के दौरान नहीं बदलता। कार्ड slide होते समय प्लेयर छिपा रहता है, इसलिए यह विकल्प समूह खुलने के बाद दिखता है।

API संदर्भ

StoriesOverlay प्रॉप्स

StoriesOverlayProps<T>

Propटाइपडिफ़ॉल्टविवरण
aria-labelstring'Stories player'डायलॉग क्षेत्र का सुलभ नाम; ओवरले खुलने पर स्क्रीन रीडर इसे पढ़ता है
chrome-placement'overlay' | 'group''overlay'प्रोग्रेस बार और हेडर कहाँ रहते हैं। 'overlay' प्लेयर के ऊपर एक कॉपी बनाता है; 'group' हर समूह की slide में एक बनाता है, जिससे दोनों समूह के साथ घूमते हैं। यह टाइप ChromePlacement नाम से export होता है।
default-image-durationnumber5000इमेज स्टोरी के ऑटो-एडवांस की अवधि, मिलीसेकंड में
desktop-layout'single' | 'carousel''single'डेस्कटॉप लेआउट। 'carousel' पड़ोसी समूहों को चल रही स्टोरी के बग़ल में कार्ड की तरह दिखाता है और समूहों के बीच सरकता है। फ़ोन पर स्टोरी हमेशा अकेली दिखती है। यह टाइप DesktopLayout के रूप में एक्सपोर्ट होता है।
enable-keyboardbooleantrueकीबोर्ड नेविगेशन चालू करता है (बाएँ/दाएँ तीर, Escape)
groupsStoriesGroup<T>[]requiredदिखाने के लिए स्टोरी समूह। प्लेयर खुले रहते हुए जोड़े गए समूह या स्टोरीज़ पकड़ लिए जाते हैं — चाहे सूची बदली जाए या उसी सूची में जोड़ा जाए।
group-transitionTransitionTransformFncubeTransitionबाहरी (समूह) स्लाइडर का ट्रांज़िशन। डेस्कटॉप कैरोसेल दिखते समय इसे अनदेखा किया जाता है, तब सरकना होता है।
hide-ui-on-pausebooleantrueदेर तक दबाकर रोकने के दौरान प्रोग्रेस बार और हेडर छिपाता है
initial-group-indexnumber0शुरू में दिखने वाले समूह का क्रमांक, शून्य से
initial-story-indexnumberरिज़्यूम, वरना 0शुरू में दिखने वाली स्टोरी का क्रमांक, शून्य से। सीधे बताया गया मान याद रखी गई जगह पर भारी पड़ता है; न दें तो यह समूह भी बाक़ी सबकी तरह वहीं से शुरू होगा जहाँ छोड़ा था।
inner-transition-durationnumber200स्टोरीज़ के बीच फ़ेड की अवधि, मिलीसेकंड में
is-openbooleanrequiredदृश्यता तय करता है और खुले रहने पर पेज की स्क्रॉलिंग रोक देता है। v-model:is-open से बाइंड करें।
min-segment-widthnumber8प्रोग्रेस के एक खंड की न्यूनतम चौड़ाई (पिक्सेल), जिसके बाद स्लाइडिंग विंडो चालू होती है
resume-story-index(groupIndex: number) => number—किसी समूह तक पहली बार पहुँचने पर वह किस स्टोरी से खुले। सिर्फ़ उसी समूह के लिए पूछा जाता है जिसे इस बार अभी तक नहीं देखा गया, और यह viewed पर भारी पड़ता है।
tap-zone-splitnumber0.3टैप क्षेत्र का बँटवारा (0–1)। बायाँ हिस्सा पीछे ले जाता है, बाक़ी आगे।
viewedStoriesViewedStateController—दर्शक ने क्या देखा है, createStoriesViewedStateController() से। समूह पहली अनदेखी स्टोरी से आगे बढ़ते हैं, दिखाई गई हर स्टोरी दर्ज होती है, और कैरोसेल के कार्ड पूरे देखे जा चुके समूह का रिंग धीमा कर देते हैं। यही कंट्रोलर StoriesRingList को भी दें।

StoriesUrlOverlay प्रॉप्स

StoriesUrlOverlayProps<T>

ऊपर के सभी प्रॉप्स लेता है, सिवाय खुलने की स्थिति वाली तिकड़ी — is-open, initial-group-index, initial-story-index — जो कंट्रोलर से आती है।

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

इवेंट

इवेंटपेलोडविवरण
@api-readyStoriesApiप्लेयर माउंट होते ही, इंपेरेटिव API के साथ चलता है
@closevoidप्लेयर बंद होने पर चलता है। StoriesUrlOverlay में तब तक पैरामीटर हट चुका होता है।
@double-tapgroupIndex: number, storyIndex: numberडबल-टैप पर, दिल के एनिमेशन के साथ चलता है
@group-changegroupIndex: numberचल रहा समूह बदलने के बाद चलता है
@pausevoidदेर तक दबाने पर या हेडर के पॉज़ बटन पर चलता है
@resumevoidउँगली हटाने पर या हेडर के प्ले बटन पर चलता है
@story-changegroupIndex: number, storyIndex: numberचल रही स्टोरी बदलने के बाद चलता है
@story-completegroupIndex: number, storyIndex: numberकिसी स्टोरी का टाइमर पूरा होने पर चलता है
@story-viewedgroupIndex: number, storyIndex: numberस्टोरी दिखने पर चलता है, उसे मिलाकर जिससे प्लेयर खुला
@update:is-openbooleanबंद होने पर चलता है; `v-model:is-open` को संभव बनाता है

v-model:is-open

एक ही बाइंडिंग से ओवरले चलाने के लिए v-model:is-open का इस्तेमाल करें। जहाँ स्पष्ट इवेंट चाहिए, वहाँ :is-open + @close की जोड़ी अब भी काम करती है।

vue
<script setup lang="ts">
import { ref } from 'vue';
import { StoriesOverlay } from '@reelkit/vue-stories-player';

const isOpen = ref(false);
</script>

<template>
  <button type="button" @click="isOpen = true">Open stories</button>

  <!-- One binding, in both directions. -->
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups" />

  <!-- The explicit pair still works when you need the event itself. -->
  <StoriesOverlay
    :is-open="isOpen"
    :groups="groups"
    @close="isOpen = false"
  />
</template>

स्कोप्ड स्लॉट

आठ स्कोप्ड स्लॉट प्लेयर के किसी भी हिस्से की जगह ले सकते हैं। हर स्लॉट को टाइप वाला स्कोप ऑब्जेक्ट मिलता है। जो स्लॉट आप नहीं देते वह डिफ़ॉल्ट पर रहता है — और वही बात उस स्लॉट पर भी लागू है जो कुछ नहीं बनाता, इसलिए झूठा हो चुका v-if खाली जगह छोड़ने के बजाय डिफ़ॉल्ट पर लौट आता है।

स्लॉटस्कोपविवरण
#error{ story, storyIndex, groupIndex }चल रही स्टोरी के ऊपर बने अंतर्निर्मित «Content unavailable» पैनल की जगह लेता है
#footer{ author, story, storyIndex }चल रहे समूह की स्टोरी के नीचे फ़ुटर जोड़ता है
#groupPreview{ group, groupIndex, story, offset, viewedCount, onOpen }डेस्कटॉप कैरोसेल के कार्ड का भीतरी हिस्सा भरता है; जगह, आकार और सरकना प्लेयर ही सँभालता है
#header{ author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose }डिफ़ॉल्ट हेडर (लेखक, पॉज़, ध्वनि, बंद करना) की जगह लेता है। chrome-placement="group" के साथ यह swipe क्षेत्र के अंदर होता है; देखें हर समूह का अपना प्रोग्रेस बार और हेडर।
#loading{ story, storyIndex, groupIndex }चल रही स्टोरी के लोड होते समय दिखने वाले डिफ़ॉल्ट हेडर स्पिनर की जगह लेता है
#navigation{ onPrevStory, onNextStory, onPrevGroup, onNextGroup }डेस्कटॉप के दोनों तीरों की जगह लेता है
#progressBar{ totalStories, activeIndex, progress, group, groupIndex, isActive }canvas प्रोग्रेस बार की जगह लेता है। activeIndex और progress core के सिग्नल हैं — उन्हें toVueRef से जोड़ें।
#slide{ story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded }डिफ़ॉल्ट इमेज और वीडियो स्लाइड की जगह लेता है
vue
<template>
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups">
    <!-- Every slot is optional; one you leave out keeps the default, and so
         does one that renders nothing. -->
    <template #header="{ author, isPaused, onTogglePause, onClose }">
      <header class="my-header">
        <img :src="author.avatar" :alt="author.name" />
        <span>{{ author.name }}</span>
        <button type="button" @click="onTogglePause">
          {{ isPaused ? 'Play' : 'Pause' }}
        </button>
        <button type="button" @click="onClose">Close</button>
      </header>
    </template>

    <template #footer="{ author, story }">
      <form class="my-reply" @submit.prevent="reply(author, story)">
        <input v-model="draft" :placeholder="`Reply to ${author.name}…`" />
      </form>
    </template>
  </StoriesOverlay>
</template>

अपनी स्लाइड

#slide के भीतर ImageStorySlide और VideoStorySlide जोड़ें — इससे ऑटोप्ले, पोस्टर कैप्चर और ध्वनि का तालमेल बना रहता है और लेआउट आपका रहता है। स्कोप के लाइफ़साइकल कॉलबैक ज़रूर बुलाएँ, वरना टाइमर कभी शुरू नहीं होगा:

कॉलबैककब बुलाएँ
onReadyइमेज लोड हो गई, या वीडियो चलने लगा। लोडिंग की स्थिति हटाता है और टाइमर शुरू करता है।
onWaitingचलते-चलते वीडियो बफ़र कर रहा है। स्पिनर दिखाता है और टाइमर रोकता है।
onErrorसामग्री लोड नहीं हुई। त्रुटि की स्थिति दिखाता है, और उस URL को टूटा हुआ याद रखा जाता है, इसलिए दोबारा खोलने पर फिर कोशिश नहीं होती।
onDurationReadyमीडिया की असली लंबाई मिलीसेकंड में बताएँ, ताकि टाइमर उतना ही चले जितना वीडियो।
onEndedमीडिया ख़त्म हुआ। अगली स्टोरी पर ले जाता है।
vue
<template>
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups">
    <!-- Compose ImageStorySlide and VideoStorySlide to keep autoplay, poster
         capture and sound sync while drawing your own layout around them.
         Pass the callbacks through, or the timer never starts. -->
    <template #slide="scope">
      <div class="my-slide">
        <VideoStorySlide
          v-if="scope.story.mediaType === 'video'"
          :src="scope.story.src"
          :poster="scope.story.poster"
          :group-index="scope.groupIndex"
          :story-index="scope.index"
          :active-group-index="scope.activeGroupIndex"
          :active-story-index="scope.activeStoryIndex"
          :on-duration-ready="scope.onDurationReady"
          :on-playing="scope.onReady"
          :on-waiting="scope.onWaiting"
          :on-ended="scope.onEnded"
          :on-error="scope.onError"
        />
        <ImageStorySlide
          v-else
          :src="scope.story.src"
          :on-load="scope.onReady"
          :on-error="scope.onError"
        />

        <!-- A carousel card draws this same slot with isActive: false, and
             nothing in it can be clicked. Keep playback or analytics behind
             the flag. -->
        <span v-if="scope.isActive" class="my-badge">{{ scope.story.id }}</span>
      </div>
    </template>
  </StoriesOverlay>
</template>
प्रीलोडर कैशिंग

अंतर्निर्मित ImageStorySlide और VideoStorySlide अगली स्टोरी को पृष्ठभूमि में पहले से लोड कर लेते हैं। जब दर्शक पहले से लोड स्टोरी पर पहुँचता है, सामग्री तुरंत दिखती है, बिना किसी लोडिंग स्पिनर के। एक बार विफल हुआ URL भी याद रखा जाता है, इसलिए टूटी हुई स्टोरी दोबारा खोलने पर फिर कोशिश करने के बजाय सीधे त्रुटि दिखती है।

अपनी नेविगेशन

vue
<template>
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups">
    <!-- Replaces both desktop arrows at once. The first node sits left of
         the story, the rest to its right. -->
    <template #navigation="{ onPrevStory, onNextStory, onPrevGroup, onNextGroup }">
      <div class="my-nav">
        <button type="button" @click="onPrevGroup">Previous user</button>
        <button type="button" @click="onPrevStory">Back</button>
      </div>
      <div class="my-nav">
        <button type="button" @click="onNextGroup">Next user</button>
        <button type="button" @click="onNextStory">Forward</button>
      </div>
    </template>
  </StoriesOverlay>
</template>

अपनी प्रोग्रेस बार

स्कोप सादे अंकों के बजाय core के सिग्नल देता है, इसलिए टाइमर प्लेयर को दोबारा रेंडर किए बिना बार चला सकता है। उन्हें @reelkit/vue के toVueRef से जोड़ें:

vue
<script setup lang="ts">
import { toVueRef, type Signal } from '@reelkit/vue';

const props = defineProps<{
  totalStories: number;
  activeIndex: Signal<number>;
  progress: Signal<number>;
}>();

// Core signals do not re-render a Vue component on their own: bridge them.
const active = toVueRef(props.activeIndex);
const progress = toVueRef(props.progress);
</script>

<template>
  <div class="my-progress">
    <span
      v-for="index in totalStories"
      :key="index"
      class="my-segment"
      :style="{
        '--fill':
          index - 1 < active
            ? '100%'
            : index - 1 === active
              ? `${progress * 100}%`
              : '0%',
      }"
    />
  </div>
</template>

अपना लोडिंग और त्रुटि रूप

vue
<template>
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups">
    <!-- Both are drawn over the active story only. Without the loading slot
         the default is the header spinner; without the error slot it is the
         built-in "Content unavailable" panel. -->
    <template #loading="{ story }">
      <p class="my-loading">Loading {{ story.mediaType }}…</p>
    </template>

    <template #error="{ story, groupIndex }">
      <div class="my-error">
        <p>That {{ story.mediaType }} would not load.</p>
        <button type="button" @click="report(groupIndex, story)">Report</button>
      </div>
    </template>
  </StoriesOverlay>
</template>
vue
<template>
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="groups"
    :viewed="viewed"
    desktop-layout="carousel"
  >
    <!-- The player still positions, scales and slides the card; this fills
         it. `offset` is negative on the left, positive on the right, and 0
         for the card passing through the center during a slide. -->
    <template #groupPreview="{ group, story, viewedCount, offset, onOpen }">
      <button type="button" class="my-card" @click="onOpen">
        <img v-if="story?.poster" :src="story.poster" alt="" />
        <span>{{ group.author.name }}</span>
        <small>{{ viewedCount }} / {{ group.stories.length }} seen</small>
        <em v-if="offset === 0">opening…</em>
      </button>
    </template>
  </StoriesOverlay>
</template>

ट्रांज़िशन

group-transition उपयोगकर्ता बदलते समय का 3D प्रभाव तय करता है। ट्रांज़िशन फ़ंक्शन @reelkit/vue से इंपोर्ट करें:

vue
<script setup lang="ts">
import { StoriesOverlay } from '@reelkit/vue-stories-player';
import {
  cubeTransition,
  flipTransition,
  fadeTransition,
  zoomTransition,
  slideTransition,
} from '@reelkit/vue';
</script>

<template>
  <!-- cubeTransition is the default; flip, fade, zoom and slide ship too. -->
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="groups"
    :group-transition="flipTransition"
  />
</template>

StoriesApi

इंपेरेटिव नियंत्रण के लिए ओवरले पर टेम्पलेट ref लगाएँ, या @api-ready इवेंट से API लें। प्लेयर बंद रहते समय किए गए कॉल कुछ नहीं करते।

vue
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { StoriesOverlay, type StoriesApi } from '@reelkit/vue-stories-player';

const isOpen = ref(false);
const player = useTemplateRef<StoriesApi>('player');

// Calls while the player is closed are safely ignored.
const skipUser = () => player.value?.nextGroup();
</script>

<template>
  <button type="button" @click="skipUser">Skip this user</button>

  <StoriesOverlay
    ref="player"
    v-model:is-open="isOpen"
    :groups="groups"
    @api-ready="(api) => api.pause()"
  />
</template>

मेथड

मेथडटाइपविवरण
nextStory()() => voidमौजूदा समूह की अगली स्टोरी पर जाता है
prevStory()() => voidमौजूदा समूह की पिछली स्टोरी पर लौटता है
nextGroup()() => voidअगले उपयोगकर्ता के समूह पर जाता है
prevGroup()() => voidपिछले उपयोगकर्ता के समूह पर जाता है
goToGroup(index)(index: number) => voidक्रमांक से किसी ख़ास समूह पर जाता है
pause()() => voidऑटो-एडवांस और प्रोग्रेस टाइमर रोकता है
resume()() => voidऑटो-एडवांस और प्रोग्रेस टाइमर फिर चालू करता है

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

डबल-टैप पर दिल का एनिमेशन चलता है और तुरंत प्रतिक्रिया दिखती है। @double-tap इवेंट समूह और स्टोरी के क्रमांक के साथ चलता है, ताकि लाइक आप अपनी स्थिति में सहेज सकें। प्लेयर खुद लाइक की स्थिति नहीं रखता।

vue
<script setup lang="ts">
import { StoriesOverlay, type StoriesGroup } from '@reelkit/vue-stories-player';

const props = defineProps<{ groups: StoriesGroup[] }>();

// The heart animation is built in; the like itself is yours to keep.
const like = (groupIndex: number, storyIndex: number) => {
  const story = props.groups[groupIndex].stories[storyIndex];
  void fetch(`/api/stories/${story.id}/like`, { method: 'POST' });
};
</script>

<template>
  <StoriesOverlay
    v-model:is-open="isOpen"
    :groups="props.groups"
    @double-tap="like"
  />
</template>

दिल के एनिमेशन में बदलाव

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

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 से छिपाकर @double-tap इवेंट से अपना एनिमेशन चलाएँ। अगर आपको heart स्लॉट चाहिए, तो GitHub Issues पर बताएँ।

उप-कंपोनेंट

एक्सपोर्ट किए गए हिस्से, जिन्हें स्लॉट के भीतर जोड़ा जा सकता है या अकेले भी इस्तेमाल किया जा सकता है: StoriesRing, StoriesRingList, StoryHeader, CanvasProgressBar, ImageStorySlide, VideoStorySlide और HeartAnimation।

कंपोनेंटयह क्या है
CanvasProgressBarcanvas पर बनी खंडों वाली प्रोग्रेस बार, जो सीधे टाइमर के सिग्नल से बनती है। बहुत सारी स्टोरीज़ वाले समूह के लिए स्लाइडिंग विंडो सँभालती है। :live="false" के साथ यह बिना animation loop के सिर्फ़ सिग्नल बदलने पर और resize के बाद बनती है।
HeartAnimationडबल-टैप वाला दिल। एनिमेशन ख़त्म होने पर complete भेजता है, ताकि पैरेंट उसे हटा सके।
ImageStorySlideobject-fit: cover वाली पूरी चौड़ाई की इमेज स्लाइड, जो लोड और त्रुटि की सूचना कॉलबैक से देती है।
StoriesRingग्रेडिएंट रिंग वाला गोल अवतार। दो स्थितियाँ: कुछ देखना बाक़ी हो तो घूमता है, सब देख लेने पर सपाट हो जाता है। click भेजता है।
StoriesRingListलेखक के नाम के साथ रिंग की क्षैतिज पंक्ति, हर समूह के लिए एक। समूह के क्रमांक के साथ select भेजता है।
StoryHeaderडिफ़ॉल्ट हेडर: अवतार, नाम, सत्यापन बैज, सापेक्ष समय, पॉज़, ध्वनि, बंद करना और लोडिंग स्पिनर।
VideoStorySlideएक ही साझा <video> एलिमेंट पर टिकी वीडियो स्लाइड, iOS पर ध्वनि की निरंतरता के लिए। ऑटोप्ले, पोस्टर फ़्रेम और अवधि की सूचना सँभालती है।
vue
<script setup lang="ts">
import {
  CanvasProgressBar,
  HeartAnimation,
  ImageStorySlide,
  StoriesRing,
  StoriesRingList,
  StoryHeader,
  VideoStorySlide,
} from '@reelkit/vue-stories-player';
</script>

<template>
  <!-- One ring, and the horizontal row of them. -->
  <StoriesRing
    :author="group.author"
    :total-stories="group.stories.length"
    :viewed-count="2"
    :size="72"
    @click="open(group)"
  />
  <StoriesRingList :groups="groups" :viewed="viewed" @select="open" />

  <!-- Inside a #header slot: the default header, with your own additions. -->
  <StoryHeader
    :author="group.author"
    :created-at="story.createdAt"
    :is-paused="isPaused"
    :is-video="story.mediaType === 'video'"
    :is-muted="isMuted"
    :on-toggle-pause="onTogglePause"
    :on-toggle-sound="onToggleSound"
    :on-close="onClose"
  />

  <!-- Inside a #progressBar slot: the canvas bar, driven by the signals. -->
  <CanvasProgressBar
    :total-stories="group.stories.length"
    :active-index="activeIndex"
    :progress="progress"
    :min-segment-width="12"
  />

  <!-- Inside a #slide slot; see the slide slot example above. -->
  <ImageStorySlide :src="story.src" :on-load="onReady" :on-error="onError" />
  <VideoStorySlide
    :src="story.src"
    :group-index="groupIndex"
    :story-index="index"
    :active-group-index="activeGroupIndex"
    :active-story-index="activeStoryIndex"
  />

  <!-- Standalone, outside the player. -->
  <HeartAnimation @complete="remove(heart.id)" />
</template>

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

उपयोग करने वाले को जो भी चाहिए वह इसी पैकेज से आता है, इसलिए @reelkit/core को सीधे इंपोर्ट करने की ज़रूरत कभी नहीं पड़ती।

स्रोतफिर से एक्सपोर्ट किया गया
@reelkit/vueObserve, SoundProvider, 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

Observe वही रेंडर सीमा है जिसे प्लेयर भीतर इस्तेमाल करता है: core का सिग्नल बदलने पर यह सिर्फ़ अपना स्लॉट दोबारा बनाता है, और इसी तरह स्टोरी बदलने पर हेडर तथा प्रोग्रेस बार ही बदलते हैं, दोनों स्लाइडर नहीं। जब toVueRef से पूरा कंपोनेंट सिग्नल का पीछा करना बहुत मोटा पड़े, तब इसे अपने स्लॉट में इस्तेमाल करें — देखें Vue API संदर्भ।

हर कंपोनेंट का प्रॉप्स टाइप भी एक्सपोर्ट होता है: StoriesOverlayProps, StoriesUrlOverlayProps, StoriesRingProps, StoriesRingListProps, StoryHeaderProps, CanvasProgressBarProps, ImageStorySlideProps और VideoStorySlideProps।

टाइप

StoryItem, AuthorInfo, StoriesGroup

typescript
interface StoryItem {
  /** Unique within its group. Stored viewed state keys on it. */
  id: string;
  mediaType: 'image' | 'video';
  src: string;
  /** Poster for a video, also used as the carousel card preview. */
  poster?: string;
  /** Overrides defaultImageDuration for this story, in milliseconds. */
  duration?: number;
  createdAt?: string | Date;
  /** Media aspect ratio (width / height). */
  aspectRatio?: number;
}

interface AuthorInfo {
  /** Stable id. Viewed state and stable-id URLs key on it. */
  id: string;
  name: string;
  avatar: string;
  verified?: boolean;
}

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

स्लॉट के स्कोप

हर स्कोप टाइप एक्सपोर्ट किया गया है, इसलिए जहाँ कंपाइलर ख़ुद अनुमान नहीं लगा पाता वहाँ स्लॉट पर टाइप लिखा जा सकता है — HeaderSlotScope, FooterSlotScope, SlideSlotScope, NavigationSlotScope, ProgressBarSlotScope, LoadingSlotScope, ErrorSlotScope और GroupPreviewSlotScope, साथ में StoriesApi:

typescript
interface HeaderSlotScope<T extends StoryItem = StoryItem> {
  author: AuthorInfo;
  story: T;
  storyIndex: number;
  isPaused: boolean;
  isMuted: boolean;
  /** True while the active story is a video — gate your sound button on it. */
  isVideo: boolean;
  groupIndex: number;
  /** False for a neighbouring group's header with chrome-placement="group". */
  isActive: boolean;
  onToggleSound: () => void;
  onTogglePause: () => void;
  onClose: () => void;
}

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

interface SlideSlotScope<T extends StoryItem = StoryItem> {
  story: T;
  index: number;
  groupIndex: number;
  /** False for the copy a carousel card draws. */
  isActive: boolean;
  size: [number, number];
  activeGroupIndex: Signal<number>;
  activeStoryIndex: Signal<number>;
  /** Report the real media length, in milliseconds. */
  onDurationReady: (durationMs: number) => void;
  onReady: () => void;
  onWaiting: () => void;
  onError: () => void;
  onEnded: () => void;
}

interface NavigationSlotScope {
  onPrevStory: () => void;
  onNextStory: () => void;
  onPrevGroup: () => void;
  onNextGroup: () => void;
}

interface ProgressBarSlotScope<T extends StoryItem = StoryItem> {
  totalStories: number;
  activeIndex: Signal<number>;
  /** 0 to 1 for the active story. Bridge it with toVueRef. */
  progress: Signal<number>;
  group: StoriesGroup<T>;
  groupIndex: number;
  /** False: the signals hold still for a neighbouring group. */
  isActive: boolean;
}

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

/** Same shape as LoadingSlotScope. */
interface ErrorSlotScope<T extends StoryItem = StoryItem> {
  story: T;
  storyIndex: number;
  groupIndex: number;
}

interface GroupPreviewSlotScope<T extends StoryItem = StoryItem> {
  group: StoriesGroup<T>;
  groupIndex: number;
  /** The story the group would open on, if it has one. */
  story: T | undefined;
  /** Negative left, positive right, 0 while sliding through the center. */
  offset: number;
  viewedCount: number;
  onOpen: () => void;
}

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

अपने स्टोरी टाइप

StoryItem को अपने फ़ील्ड से बढ़ाएँ और उसी टाइप से समूहों को टाइप करें। StoriesOverlay और StoriesUrlOverlay React प्लेयर की तरह जेनेरिक हैं: स्टोरी टाइप groups से निकाला जाता है, इसलिए जिन समूहों की स्टोरी StoryItem से मेल नहीं खातीं, वे टाइप जाँच में फ़ेल होते हैं। Vue में स्लॉट यह टाइप नहीं ले जा सकते, इसलिए जब तक आप टाइप न लिखें, स्लॉट के स्कोप बिना टाइप के रहते हैं। स्लॉट के स्कोप पर टाइप लिखें, ताकि टेम्पलेट के भीतर भी अतिरिक्त फ़ील्ड दिखते रहें। यह टाइप groups से मिलाकर जाँचा नहीं जाता, इसलिए दोनों को एक जैसा रखें:

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  StoriesOverlay,
  type SlideSlotScope,
  type StoriesGroup,
  type StoryItem,
} from '@reelkit/vue-stories-player';

const isOpen = ref(false);

interface PromoStory extends StoryItem {
  title?: string;
  ctaHref?: string;
}

const groups: StoriesGroup<PromoStory>[] = [
  {
    author: { id: 'shop', name: 'Shop', avatar: '/shop.jpg' },
    stories: [
      {
        id: 'sale',
        mediaType: 'image',
        src: '/sale.jpg',
        title: 'Spring sale',
        ctaHref: '/sale',
      },
    ],
  },
];
</script>

<template>
  <StoriesOverlay v-model:is-open="isOpen" :groups="groups">
    <!-- Annotate the scope with your own item type to keep the slot typed. -->
    <template #slide="scope: SlideSlotScope<PromoStory>">
      <article class="promo">
        <h2>{{ scope.story.title }}</h2>
        <a v-if="scope.story.ctaHref" :href="scope.story.ctaHref">Shop now</a>
      </article>
    </template>
  </StoriesOverlay>
</template>

CSS क्लास

क्लास सादी हैं (scoped नहीं), इसलिए @reelkit/vue-stories-player/styles.css के बाद लोड होने वाली कोई भी स्टाइलशीट इनमें से किसी को भी बदल सकती है। ये @reelkit/react-stories-player जैसी ही हैं, इसलिए एक बाइंडिंग के लिए लिखी स्टाइलशीट दूसरी पर भी चलती है। रंग, आकार और z-index के लिए थीमिंग वाली कस्टम प्रॉपर्टीज़ बेहतर हैं।

क्लासकंपोनेंटविवरण
.rk-stories-overlayOverlayपूरी स्क्रीन की स्थिर पृष्ठभूमि (रंग, z-index)
.rk-stories-overlay--carouselOverlayडेस्कटॉप कैरोसेल दिखने वाला ओवरले
.rk-stories-overlay--slidingOverlayकैरोसेल सरक रहा है; प्लेयर का कैनवास छिपा है
.rk-stories-swipe-wrapperOverlayस्वाइप करके बंद करने का रैपर (नेविगेशन बटन और कैनवास रखता है)
.rk-stories-containerOverlayगोल कोनों वाला स्टोरी कैनवास (स्थिति, overflow)
.rk-stories-ui-layerOverlayइंटरफ़ेस का कंटेनर (हेडर, प्रोग्रेस)। chrome-placement="group" के साथ हर समूह की slide के अंदर
.rk-stories-ui-layer--hiddenOverlayइंटरफ़ेस छिपी अवस्था (hide-ui-on-pause बदलता है)
.rk-stories-errorOverlayत्रुटि की स्थिति (बीच में आइकन और पाठ)
.rk-stories-error-textOverlayत्रुटि संदेश का पाठ
.rk-stories-nav-btnNavigationडेस्कटॉप का अगला/पिछला तीर
.rk-stories-progress-barCanvasProgressBarcanvas प्रोग्रेस बार रखने वाला रैपर
.rk-stories-slide-wrapperGroupस्टोरीज़ का एक समूह (बाहरी स्लाइड)
.rk-stories-storyStoryएक स्टोरी (भीतरी स्लाइड की जड़)
.rk-stories-headerStoryHeaderहेडर पट्टी (अवतार, नाम, क्रियाएँ)
.rk-stories-header--hiddenStoryHeaderहेडर की छिपी अवस्था
.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-imageImageStorySlideइमेज स्टोरी का एलिमेंट
.rk-stories-videoVideoStorySlideवीडियो स्टोरी का कंटेनर
.rk-stories-video-elementVideoStorySlideसाझा <video> एलिमेंट
.rk-stories-video-posterVideoStorySlideवीडियो का पोस्टर (चलने पर मिट जाता है)
.rk-stories-video-poster--visibleVideoStorySlideपोस्टर दिखने की अवस्था (चलने से पहले)
.rk-stories-heartHeartAnimationडबल-टैप पर दिल का उछलता एनिमेशन
.rk-stories-ringStoriesRingस्टोरी रिंग (एनिमेटेड ग्रेडिएंट किनारे वाला अवतार)
.rk-stories-ring--activeStoriesRingजिस रिंग में देखना बाक़ी है (चलता है)
.rk-stories-ring-avatarStoriesRingरिंग के भीतर की अवतार इमेज
.rk-stories-ring-listStoriesRingListरिंग की क्षैतिज सूची का कंटेनर
.rk-stories-ring-list-itemStoriesRingListरिंग और नाम का स्तंभ
.rk-stories-ring-list-nameStoriesRingListहर रिंग के नीचे लेखक का नाम
.rk-stories-carouselCarouselप्लेयर के पीछे कार्ड की परत
.rk-stories-carousel--instantCarouselसरकने का पहला फ़्रेम, बिना ट्रांज़िशन के रखा गया
.rk-stories-cardCarouselएक रखा हुआ कार्ड
.rk-stories-card--centerCarouselसरकने के दौरान बीच की जगह पर आया कार्ड
.rk-stories-card--hiddenCarouselएक ओर दिखने वाले दो कार्ड से आगे का कार्ड
.rk-stories-card-buttonCarouselकार्ड की डिफ़ॉल्ट सामग्री (समूह खोलने वाला बटन)
.rk-stories-card-imageCarouselझलक की तस्वीर (पोस्टर या इमेज)
.rk-stories-card-frameCarouselबिना पोस्टर या इमेज वाली स्टोरी, जिसे #slide स्लॉट प्लेयर के आकार में बनाता है और फिर छोटा किया जाता है
.rk-stories-card-scrimCarouselतस्वीर पर धुँधलेपन की परत
.rk-stories-card-infoCarouselरिंग, लेखक का नाम और समय वाला स्तंभ
.rk-stories-card-nameCarouselलेखक का नाम
.rk-stories-card-timeCarouselसापेक्ष समय का पाठ

थीमिंग

हर रंग, आकार, z-index और ट्रांज़िशन एक CSS कस्टम प्रॉपर्टी में रहता है, जिसका डिफ़ॉल्ट :root पर है। :root पर या ओवरले के किसी भी पूर्वज पर एक या कई को बदल दें — कंपोनेंट के स्रोत को छुए बिना थीम बदल जाएगी। ये टोकन @reelkit/react-stories-player जैसे ही हैं, इसलिए थीम एक बाइंडिंग से दूसरी में चली जाती है।

टोकनडिफ़ॉल्टक्या तय करता है
--rk-stories-overlay-bg#000पूरी स्क्रीन की पृष्ठभूमि का रंग
--rk-stories-overlay-z9999ओवरले का z-index
--rk-stories-container-radius12pxस्टोरी कैनवास के गोल कोने (डेस्कटॉप)
--rk-stories-container-radius-mobile0फ़ोन पर, 768px चौड़ाई तक, स्टोरी कैनवास के कोने
--rk-stories-swipe-gap16pxनेविगेशन बटन और स्टोरी कैनवास के बीच की दूरी
--rk-stories-top-shade-height120pxहेडर के पीछे ऊपरी ग्रेडिएंट की ऊँचाई
--rk-stories-top-shade-bglinear-gradient(to bottom, rgba(0,0,0,0.5) 0%, transparent 100%)ऊपरी ग्रेडिएंट का रंग
--rk-stories-ui-transition200mshide-ui-on-pause बदलने पर फ़ेड की अवधि
--rk-stories-ui-z15UI परत (प्रोग्रेस बार और हेडर) का 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-nav-z20डेस्कटॉप नेविगेशन बटन का 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-text-size13pxत्रुटि संदेश का फ़ॉन्ट आकार
--rk-stories-error-gap12pxत्रुटि के आइकन और उसके पाठ के बीच की दूरी
--rk-stories-error-z5त्रुटि की स्थिति का z-index
--rk-stories-video-bg#000<video> के पीछे की पट्टियों का रंग
--rk-stories-video-poster-transition200msवीडियो चलने पर पोस्टर के मिटने की अवधि
--rk-stories-progress-bar-padding8px 8px 0प्रोग्रेस बार के चारों ओर का अंतर
--rk-stories-progress-bar-z10प्रोग्रेस बार का z-index
--rk-stories-header-top18pxस्टोरी के ऊपर से हेडर की दूरी
--rk-stories-header-padding12px 16pxहेडर पंक्ति का भीतरी अंतर
--rk-stories-header-z5हेडर का z-index
--rk-stories-header-transition200msछिपते समय हेडर के फ़ेड की अवधि
--rk-stories-header-gap8pxअवतार, नाम और समय के बीच की दूरी
--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-btn-fg#fffहेडर की क्रिया आइकनों का रंग (बंद, म्यूट, पॉज़)
--rk-stories-header-btn-padding4pxहेडर के क्रिया बटन का भीतरी अंतर
--rk-stories-header-actions-gap8pxहेडर के क्रिया बटनों के बीच की दूरी
--rk-stories-header-spinner-size20pxलोडिंग स्पिनर का व्यास
--rk-stories-header-spinner-fg#fffलोडिंग स्पिनर के चाप का रंग
--rk-stories-header-spinner-trackrgba(255, 255, 255, 0.3)लोडिंग स्पिनर के ट्रैक का रंग
--rk-stories-header-spinner-duration0.8sलोडिंग स्पिनर के एक चक्कर की अवधि
--rk-stories-heart-duration800msदिल के उभरने और मिटने की अवधि
--rk-stories-heart-z20डबल-टैप वाले दिल का z-index
--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 न दिया जाए
--rk-stories-ring-list-gap12pxसूची में रिंगों के बीच की दूरी
--rk-stories-ring-list-padding12pxरिंग सूची के चारों ओर का भीतरी अंतर
--rk-stories-ring-list-item-gap4pxरिंग और उसके नाम के बीच की दूरी
--rk-stories-ring-list-name-size12pxहर रिंग के नीचे नाम का फ़ॉन्ट आकार
--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कैरोसेल के सरकने की अवधि
css
/* Loaded after @reelkit/vue-stories-player/styles.css. Every token has a
   default on :root, so override only what you want to change. The names
   match @reelkit/react-stories-player, 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")। स्क्रीन रीडर क्या बोले, यह बदलने के लिए aria-label सेट करें; डिफ़ॉल्ट «Stories player» है।

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

डेस्कटॉप कैरोसेल में हर डिफ़ॉल्ट कार्ड एक बटन है, जिसका लेबल «Open stories by» और लेखक का नाम होता है। टैब क्रम में कार्ड प्लेयर के नियंत्रणों के बाद आते हैं, और सरकने के दौरान क्रम से बाहर रहते हैं; जो कार्ड अपना समूह खोलता है वह फ़ोकस पेज पर छोड़ने के बजाय डायलॉग को लौटा देता है।

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

बंद रहते हुए ओवरले कुछ नहीं बनाता, और StoriesRingList सर्वर पर हर रिंग को अनदेखा दिखाता है: देखी गई स्टोरीज़ का स्टोरेज माउंट के बाद ही पढ़ा जाता है, इसलिए सर्वर का मार्कअप पहले क्लाइंट रेंडर से मेल खाता है। पूरी तस्वीर SSR गाइड में है।

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

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