Stories Core

@reelkit/react-stories-player के पीछे का इंजन। शुद्ध TypeScript, किसी फ़्रेमवर्क पर निर्भर नहीं। Angular, Vue या vanilla JS के लिए stories प्लेयर बनाने में इसका इस्तेमाल करें।

फ़्रेमवर्क से स्वतंत्र
शुद्ध TypeScript, किसी DOM फ़्रेमवर्क पर निर्भरता नहीं
दो-स्तर का नेविगेशन
ग्रुप + हर ग्रुप के अंदर की stories
Story की टाइमिंग
core टाइमर पर अपने-आप आगे बढ़ना, pause और resume के साथ
Canvas प्रोग्रेस
खिसकती विंडो वाला, Retina के लिए तैयार segmented प्रोग्रेस बार
टैप ज़ोन
बाएँ/दाएँ टैप की पहचान, जिसे कॉन्फ़िगर किया जा सकता है
Reactive सिग्नल
@reelkit/core के signal primitives पर बना

इंस्टॉलेशन

bash
npm i @reelkit/stories-core

Stories कंट्रोलर

createStoriesController(config, events?) एक StoriesController लौटाता है, जो ग्रुप और stories के बीच नेविगेशन संभालता है। यह pause/resume स्टेट पर नज़र रखता है, हर ग्रुप में आख़िरी देखी गई story याद रखता है, और हर बदलाव पर callbacks चलाता है।

कॉन्फ़िग (StoriesControllerConfig)

प्रॉपर्टीटाइपडिफ़ॉल्टविवरण
groupCountnumberज़रूरीstory ग्रुप की कुल संख्या
storyCountsnumber[]ज़रूरीहर ग्रुप में stories की संख्या
initialGroupIndexnumber0शुरुआती ग्रुप index
initialStoryIndexnumberresumeStoryIndex(initialGroupIndex), नहीं तो 0ग्रुप के अंदर शुरुआती story index। इसे देने पर यह याद रखी गई किसी भी स्थिति से ऊपर रहता है; न देने पर खुलने वाला ग्रुप भी बाकी ग्रुप की तरह वहीं से शुरू होता है।
defaultImageDurationnumber5000इमेज stories के अपने-आप आगे बढ़ने की डिफ़ॉल्ट अवधि, ms में
resumeStoryIndex(groupIndex: number) => numberundefinedवह story जिस पर न देखा गया ग्रुप खुलता है, ग्रुप में मौजूद stories तक सीमित

इवेंट (StoriesControllerEvents)

इवेंटटाइपविवरण
onStoryChange(groupIndex, storyIndex) => voidसक्रिय story बदलने पर चलता है
onGroupChange(groupIndex) => voidसक्रिय ग्रुप बदलने पर चलता है
onStoryViewed(groupIndex, storyIndex) => voidकिसी story के दिखने पर चलता है
onStoryComplete(groupIndex, storyIndex) => voidstory का टाइमर पूरा होने पर चलता है (आगे बढ़ने से पहले)
onComplete() => voidआख़िरी ग्रुप की आख़िरी story ख़त्म होने पर चलता है
onClose() => voidजब overlay बंद होना चाहिए, तब चलता है

स्टेट (reactive signals)

सिग्नलटाइपविवरण
state.activeGroupIndexSignal<number>अभी सक्रिय ग्रुप का index
state.activeStoryIndexSignal<number>ग्रुप के अंदर अभी सक्रिय story का index
state.isPausedSignal<boolean>अपने-आप आगे बढ़ना रुका है या नहीं

मेथड

मेथडटाइपविवरण
nextStory()() => voidग्रुप के अंदर आगे बढ़ता है; सीमा पार करके अगले ग्रुप में जाता है
prevStory()() => voidग्रुप के अंदर पीछे जाता है; सीमा पार करके पिछले ग्रुप में जाता है
nextGroup()() => voidअगले ग्रुप पर जाता है, आख़िरी देखी गई story से शुरू करके
prevGroup()() => voidपिछले ग्रुप पर जाता है, आख़िरी देखी गई story से शुरू करके
goToGroup(index)(number) => voidindex से किसी खास ग्रुप पर सीधे जाता है
pause()() => voidअपने-आप आगे बढ़ना रोकता है
resume()() => voidअपने-आप आगे बढ़ना फिर शुरू करता है
onStoryTimerComplete()() => voidटाइमर पूरा होने पर कॉल होता है; पहले onStoryComplete चलाता है, फिर आगे बढ़ता है
getLastStoryIndex(groupIndex)(number) => numberग्रुप कहाँ खुलेगा: इस सेशन में जिस story पर छोड़ा गया, नहीं तो वह जो resumeStoryIndex बताए
reportInitialView()() => voidप्लेयर जिस story पर खुला, उसे एक बार देखी गई के रूप में दर्ज करता है। mount के बाद कॉल करें, रेंडर के दौरान नहीं।
updateConfig({ groupCount, storyCounts })(config) => voidप्लेयर खुला रहते हुए feed बदलने पर groups और stories की गिनती बदल देता है, ताकि बाद में लोड हुए groups तक पहुँचा जा सके। कोई event नहीं भेजता, हर group जहाँ छोड़ा गया था वह याद रखता है, और feed छोटा होने पर positions को उसके भीतर ले आता है।

उदाहरण

typescript
import { createStoriesController } from '@reelkit/stories-core';
import { createTimerController, reaction } from '@reelkit/core';

const groups = [
  { stories: ['s1', 's2', 's3'] },
  { stories: ['s4', 's5'] },
];

const controller = createStoriesController(
  {
    groupCount: groups.length,
    storyCounts: groups.map((g) => g.stories.length),
    defaultImageDuration: 5000,
  },
  {
    onStoryChange(groupIndex, storyIndex) {
      console.log('Story changed:', groupIndex, storyIndex);
    },
    onComplete() {
      console.log('All stories viewed');
    },
    onClose() {
      console.log('Overlay closed');
    },
  },
);

// Wire up a timer for auto-advance
const timer = createTimerController({
  duration: 5000,
  onComplete: () => controller.onStoryTimerComplete(),
});

// React to story changes and restart the timer
const dispose = reaction(
  () => [
    controller.state.activeGroupIndex,
    controller.state.activeStoryIndex,
  ],
  () => timer.start(),
);

// Start playback
timer.start();

// Navigation
controller.nextStory();
controller.pause();
controller.resume();

// Cleanup
dispose();
timer.dispose();

टाइमर कंट्रोलर

हर story की प्रोग्रेस चलाने वाला टाइमर @reelkit/core में createTimerController के रूप में रहता है, और lightbox स्लाइडशो के साथ साझा है। इसे @reelkit/core से या अपनी binding (@reelkit/react, @reelkit/vue, @reelkit/angular) से import करें; यह पैकेज अब इसे export नहीं करता। इसके config, स्टेट और मेथड के लिए core API में ऑटोप्ले देखें, और इसे onStoryTimerComplete से जोड़ने के लिए ऊपर का Stories कंट्रोलर उदाहरण देखें।

Canvas प्रोग्रेस रेंडरर

createCanvasProgressRenderer(config?) canvas पर segmented प्रोग्रेस बार बनाता है। यह Retina डिस्प्ले के हिसाब से स्केल होता है, @reelkit/core के observeElementSize से अपने कंटेनर की चौड़ाई पर नज़र रखता है, और segments न समाने पर खिसकती विंडो इस्तेमाल करता है।

कॉन्फ़िग (CanvasProgressRendererConfig)

प्रॉपर्टीटाइपडिफ़ॉल्टविवरण
gapnumber2segments के बीच पिक्सेल में दूरी
barHeightnumber2पिक्सेल में बार की ऊँचाई
minSegmentWidthnumber8segment की न्यूनतम चौड़ाई, जिसके बाद खिसकती विंडो शुरू होती है
bgColorstring'rgba(255,255,255,0.3)'न भरे segments का बैकग्राउंड रंग
fillColorstring'#ffffff'पूरे/सक्रिय segments का भरने वाला रंग
onResize() => void-कंटेनर का साइज़ बदलने और canvas के दोबारा नापे जाने के बाद कॉल होता है। canvas का साइज़ बदलने से वह साफ़ हो जाता है, इसलिए यहाँ दोबारा ड्रॉ करें।

मेथड

createCanvasProgressRenderer एक CanvasProgressRenderer लौटाता है। जब आप रेंडरर रखने वाली किसी कंपोनेंट फ़ील्ड या ref को टाइप करें, तो यही ऑब्जेक्ट रखें:

सदस्यटाइपविवरण
attach(canvas)(HTMLCanvasElement) => voidcanvas एलिमेंट से जुड़ता है; parent के साइज़ पर नज़र रखना शुरू करता है
draw(totalStories, activeIndex, progress)(number, number, number) => voidदी गई स्टेट के लिए प्रोग्रेस बार बनाता है
widthnumber (readonly)CSS पिक्सेल में अभी नापी गई चौड़ाई
dispose()() => voidparent के साइज़ पर नज़र रखना बंद करता है और अंदरूनी स्टेट साफ़ करता है

उदाहरण

typescript
import { createCanvasProgressRenderer } from '@reelkit/stories-core';

const renderer = createCanvasProgressRenderer({
  gap: 2,
  barHeight: 2,
  fillColor: '#ffffff',
  bgColor: 'rgba(255, 255, 255, 0.3)',
});

// Attach to a canvas element
const canvas = document.querySelector('canvas')!;
renderer.attach(canvas);

// Draw on each animation frame
let frameId: number;

function loop() {
  const totalStories = 5;
  const activeIndex = 2;
  const progress = timer.progress.value; // 0-1

  renderer.draw(totalStories, activeIndex, progress);
  frameId = requestAnimationFrame(loop);
}

frameId = requestAnimationFrame(loop);

// Cleanup
cancelAnimationFrame(frameId);
renderer.dispose();

देखे गए आइटम का कंट्रोलर

createStoriesViewedStateController(config) एक ही कॉल में वह सब देता है जो किसी feed को देखी गई चीज़ें याद रखने के लिए चाहिए: rings की गिनती, हर group कहाँ से दोबारा शुरू हो, और देखे जाने का रिकॉर्ड, ऐसे storage पर जो reload के बाद भी बना रहता है। यह कोई framework import नहीं करता, इसलिए React, Vue और Angular इसे एक ही तरह इस्तेमाल करते हैं: इसे वहाँ एक बार बनाएँ जहाँ feed रहता है, और ring list तथा player को दे दें, जो इसे खुद attach करते हैं और follow करते हैं। यह core के createViewedStateController और स्थिर ids वाली two-axis key पर बना है; जब यह फिट न हो तो सीधे उन्हें इस्तेमाल करें। Entry देखे जाने की गिनती नहीं, बल्कि सबसे दूर पहुँची story का नाम है, इसलिए फ़ीड का क्रम बदलने पर भी ग्रुप की जगह बनी रहती है, जबकि ग्रुप के बीच से हटाई गई story उसकी गिनती घटा देती है और उसकी ring फिर से जल उठती है।

कॉन्फ़िग (StoriesViewedStateControllerConfig)

प्रॉपर्टीटाइपडिफ़ॉल्टविवरण
storageKeystringज़रूरीवह storage key जिसके नीचे entries लिखी जाती हैं
groups() => StoriesGroup<T>[]ज़रूरीमौजूदा groups पढ़ता है। यह getter है और हर बार कॉल होता है, इसलिए और groups लोड करने वाले feed को बस उन्हें लौटाना होता है। इसे ताज़ा रखें: पहले array पर अटका getter बाद में लोड हुए हर group की गिनती गलत करता है।
keyUrlKey<Id, TwoAxisPosition>स्थिर idsstorage में position कैसे लिखी जाती है। वही key पास करें जो URL कंट्रोलर इस्तेमाल करता है, तो स्टोर की गई entry शेयर किए गए लिंक जैसी पढ़ी जाती है।
storageStorageAdapterlocalStorageentries कहाँ रखी जाती हैं। अपना adapter देना, अपना देखे-गए का store जोड़ने का तरीका भी है।
ttlMsnumberकभी नहींकिसी group की entry आख़िरी बार दर्ज होने के बाद कितनी देर याद रहे। हर group अपनी घड़ी से expire होता है।
maxTracksnumberसभी रहते हैंकितने groups रखने हैं। इससे ऊपर, सबसे पुराना दर्ज group अगली बार लिखते समय हटा दिया जाता है।

storageKey, key और storage एक ही बार पढ़े जाते हैं, कंट्रोलर बनाते समय। storage बदलने के लिए दूसरा कंट्रोलर बनाएँ।

StoriesViewedStateController

सदस्यटाइपविवरण
viewedStateSubscribable<Map<string, number>>हर author id के लिए देखी गई stories, signal के रूप में। attach() से पहले खाली।
controllerViewedStateController<TwoAxisPosition>नीचे का store, उन ज़रूरतों के लिए जो यहाँ शामिल नहीं हैं
resumeStoryIndex(groupIndex)(number) => numberपहली न देखी गई story, या ग्रुप पूरा देख लेने पर 0
markViewed(groupIndex, storyIndex)(number, number) => voidstory को देखी गई के रूप में दर्ज करता है। player हर दिखाई गई story के लिए इसे कॉल करता है।
forget()() => voidदेखा हुआ सब भुला देता है, यहाँ भी और storage में भी
attach()() => Disposestorage पढ़ता है और दूसरे tabs को follow करता है। गिनती रखता है, इसलिए ring list और player किसी भी क्रम में attach और unmount हो सकते हैं। player के components इसे खुद कॉल करते हैं।

उदाहरण

typescript
import { createStoriesViewedStateController } from '@reelkit/stories-core';

// Once, where the feed lives. The getter is read every time, so it has to
// return the groups as they are now.
const viewed = createStoriesViewedStateController({
  storageKey: 'stories-seen',
  groups: () => groups,
});

// The player components do this themselves when given `viewed`.
const detach = viewed.attach();

viewed.viewedState.value; // Map { 'user_42' => 2 }, and a signal to follow
viewed.resumeStoryIndex(0); // 2 — the first story not yet seen
viewed.markViewed(0, 2); // furthest point wins; a rewatch never rewinds
viewed.forget(); // clears it all

detach();

यूटिलिटी फ़ंक्शन

टैप ज़ोन पहचानने और प्रोग्रेस बार के हिसाब के लिए शुद्ध फ़ंक्शन।

फ़ंक्शनटाइपविवरण
getTapAction(tapX, containerWidth, splitRatio?)(number, number, number?) => 'prev' | 'next'स्थिति के आधार पर तय करता है कि टैप 'prev' चलाएगा या 'next'। डिफ़ॉल्ट splitRatio 0.3 है।
getSegments(totalStories, activeIndex, progress)(number, number, number) => SegmentState[]प्रोग्रेस बार के हर segment की स्थिति और भराव का प्रतिशत निकालता है
getVisibleWindow(totalStories, activeIndex, progress, containerWidth, minSegmentWidth?, gap?)(number, number, number, number, number?, number?) => VisibleWindowकुल संख्या कंटेनर की क्षमता से ज़्यादा होने पर segments की दिखने वाली खिसकती विंडो निकालता है

लेआउट हेल्पर

हर बाइंडिंग का प्लेयर इन्हीं शुद्ध फ़ंक्शन को साझा करता है, इसलिए React, Vue और Angular में आकार और सजावट एक जैसी रहती है। इन्हें बाइंडिंग बुलाती है; ऐप को इनकी ज़रूरत सिर्फ़ तब पड़ती है जब वह अपना प्लेयर बना रहा हो।

आकार

फ़ंक्शनटाइपविवरण
getStoriesSize()() => [number, number]स्टोरी कैनवास का आकार। फ़ोन पर पूरी स्क्रीन; डेस्कटॉप पर विंडो की ऊँचाई में से 16px का हाशिया घटाकर 9:16 में, और तभी सँकरा होता है जब कैनवास तथा दोनों तीर आड़े में न समाएँ। window न हो (सर्वर रेंडरिंग) तो [0, 0] लौटाता है।
parseDurationMs(value)(string) => numberगणना किए गए transition-duration की सूची में सबसे लंबा समय, मिलीसेकंड में। जो समय नहीं है वह शून्य गिना जाता है।

getStoriesSize जिस फ़ोन breakpoint का इस्तेमाल करता है वह isMobileWidth है, जो @reelkit/core और हर binding से export होता है: 768px और उससे कम फ़ोन है। देखें साइज़ पहचान।

ऑफ़सेट चल रहे समूह के सापेक्ष हैं: बाईं ओर ऋणात्मक, दाईं ओर धनात्मक, और 0 का मतलब चल रही स्टोरी ही।

फ़ंक्शनटाइपविवरण
getCardSize(activeSize)([number, number]) => [number, number]बग़ल के कार्ड का आकार: चल रही स्टोरी की ऊँचाई का 0.4, उसी 9:16 अनुपात में।
getCarouselSlot(offset, activeSize)(number, [number, number]) => CarouselSlotविंडो के केंद्र के सापेक्ष कार्ड कहाँ बैठता है: { x, width, height }। हर ओर का पहला कार्ड तीर से आगे रहता है, और उसके बाद हर कार्ड एक कार्ड की चौड़ाई तथा एक अंतर और आगे।
getCardOffsets(activeGroupIndex, groupCount)(number, number) => number[]ठहरी हुई स्थिति में चल रहे समूह के आसपास बनने वाले कार्डों के ऑफ़सेट, हर ओर अधिकतम दो, चल रहे समूह को छोड़कर।
isCardShown(offset)(number) => booleanइतनी दूरी का कार्ड धुँधला होने के बजाय बनाया जाएगा या नहीं।
getSlotOffset(offset)(number) => numberकार्ड जिस जगह जाता है, दिखने वाली सीमा से एक क़दम आगे तक सीमित, ताकि कार्ड आख़िरी दिखते कार्ड के बग़ल से सरककर आए।
getSlideGroupIndexes(from, to, groupCount)(number, number, number) => number[]दो समूहों के बीच सरकने पर कौन-से समूह बनते हैं: दोनों सिरों के आसपास के कार्ड, समूह के क्रम में। लंबी छलाँग जिन समूहों के ऊपर से गुज़रती है, वे छोड़ दिए जाते हैं।

प्रस्तुति

फ़ंक्शनटाइपविवरण
getRingPresentation(options)(RingPresentationOptions) => RingPresentationtotalStories, viewedCount और size से स्टोरी रिंग का क्लास नाम, इनलाइन स्टाइल और अवतार का आकार। कुछ देखना बाक़ी हो तो घूमता हुआ शंक्वाकार ग्रेडिएंट, पूरा देख लेने पर सपाट viewedColor, और ख़ाली समूह पर कोई रिंग नहीं। आकार px इकाई के साथ आते हैं, इसलिए यह स्टाइल ऑब्जेक्ट किसी भी फ़्रेमवर्क में चलता है। रंग --rk-stories-ring-active-gradient और --rk-stories-ring-viewed-color टोकन से आते हैं, इसलिए पेज इन्हें CSS से बदल सकता है; gradientColors और viewedColor एक रिंग का रंग तय करते हैं।
kStoriesRingSize, kStoriesRingListRingSize, kStoriesCardRingSizenumberहर binding डिफ़ॉल्ट रूप से जिन रिंग व्यासों को बनाता है, पिक्सेल में: अकेली रिंग के लिए 68, रिंग सूची में 64, डेस्कटॉप carousel कार्ड पर 52। प्लेयरों से मेल के लिए इनमें से एक को size के रूप में दें।
getPreviewSource(story)(StoryItem | undefined) => string | undefinedकैरोसेल कार्ड जो तस्वीर दिखाता है: स्टोरी का poster, तस्वीर होने पर वही तस्वीर, और बिना पोस्टर वाले वीडियो के लिए कुछ नहीं। बिना तस्वीर वाला कार्ड उसी सादे कार्ड पर लौट आता है जो दिखाने को कुछ न रखने वाली स्टोरी के लिए बनता है।
formatTimeAgo(date)(string | Date) => stringहेडर और कैरोसेल कार्ड जो छोटा सापेक्ष समय दिखाते हैं: now, 5m, 3h, 2d, 2w।

इनके साथ CarouselSlot, RingPresentation और RingPresentationOptions टाइप भी एक्सपोर्ट होते हैं।

टाइप

@reelkit/stories-core से export होने वाली सभी टाइप डेफ़िनिशन।

typescript
type MediaType = 'image' | 'video';

interface StoryItem {
  id: string;
  mediaType: MediaType;
  src: string;
  poster?: string;
  duration?: number;
  createdAt?: string | Date;
  aspectRatio?: number;
}

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

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

type SegmentStatus = 'completed' | 'active' | 'upcoming';

interface SegmentState {
  status: SegmentStatus;
  fillPercentage: number; // 0-100
}

interface VisibleWindow {
  startIndex: number;
  endIndex: number;
  segments: SegmentState[];
}

type TapAction = 'prev' | 'next';

interface CarouselSlot {
  x: number;
  width: number;
  height: number;
}

interface RingPresentationOptions {
  totalStories: number;
  viewedCount: number;
  size: number; // px
  gradientColors?: string[];
  viewedColor?: string;
}

interface RingPresentation {
  avatarSize: number;
  className: string;
  style: Record<string, string>;
}

यह पैकेज इंजन है, प्लेयर नहीं: इसमें state machine और ज्यामिति है, और रेंडरिंग binding पर छोड़ दी जाती है। पहले कोई तैयार प्लेयर आज़माएँ, और अपना प्लेयर बनाते समय फ़्रेमवर्क binding का सहारा लें — virtualized Reel, signal bridge, overlay helpers।

अगले कदम