Core गाइड

@reelkit/core पैकेज फ़्रेमवर्क से स्वतंत्र स्लाइडर logic देता है। इससे कस्टम इंटीग्रेशन बनाएँ या अंदर की आर्किटेक्चर समझें।

आर्किटेक्चर का परिचय

core factory फ़ंक्शन के साथ controller पैटर्न इस्तेमाल करता है। कोई class नहीं — सब closures से लौटाए गए साधारण objects हैं। कोई dependency नहीं। core इन सबके बीच तालमेल रखता है:

  • SliderController — स्टेट का केंद्रीय प्रबंधन और नेविगेशन
  • GestureController — टच/pointer से खींचने को संभालना
  • KeyboardController — तीर वाली कुंजियाँ और Escape
  • WheelController — debounce के साथ माउस व्हील

createSliderController

एक नया स्लाइडर कंट्रोलर instance बनाता है, जो स्लाइडर की सारी स्टेट और व्यवहार संभालता है।

typescript
import { createSliderController } from '@reelkit/core';

const controller = createSliderController(
  {
    count: 10,
    direction: 'vertical',
    enableWheel: true,
    transitionDuration: 300,
  },
  {
    onAfterChange: (index) => console.log('Changed to:', index),
  }
);

// Attach to DOM element
controller.attach(element);
controller.observe();

कंट्रोलर के मेथड

typescript
// Go to specific index
controller.goTo(5);           // instant
controller.goTo(5, true);     // animated, returns Promise

// Navigate to next/previous
controller.next();
controller.prev();

Lifecycle

typescript
// Connect to DOM element
controller.attach(element);

// Start gesture, keyboard, and wheel observation
controller.observe();

// Stop gesture, keyboard, and wheel observation
controller.unobserve();

// Detach DOM listeners (reversible — use for React effect cleanup)
controller.detach();

// Permanent teardown (use for Angular onDestroy)
controller.dispose();

// Recalculate positions
controller.adjust();

// Update size
controller.setPrimarySize(600);

स्टेट अपडेट

typescript
// Update configuration
controller.updateConfig({
  count: 20,
  loop: true,
});

वर्चुअलाइज़ेशन

core किसी भी समय DOM में सिर्फ़ 3 स्लाइड रेंडर करता है (मौजूदा, पिछली, अगली)। Range extractor तय करता है कि रेंडर होने वाली विंडो में कौन-से index शामिल हों:

typescript
import { defaultRangeExtractor } from '@reelkit/core';

// Default: renders current ± 1 (3 DOM nodes)
const indexes = defaultRangeExtractor(currentIndex, count);

// Custom: skip hidden slides by shifting to next valid index
const hiddenSlides = new Set([2, 5]);

const skipHiddenExtractor = (current: number, count: number) => {
  const result: number[] = [];
  // Collect prev, current, next — skip hidden, shift forward
  for (let i = current - 1, added = 0; added < 3 && i < count; i++) {
    if (i >= 0 && !hiddenSlides.has(i)) {
      result.push(i);
      added++;
    }
  }
  return result;
};

नतीजा हमेशा ज़्यादा से ज़्यादा 3 index तक सीमित रहता है। अगर आपका extractor इससे ज़्यादा लौटाता है, तो core मौजूदा स्लाइड के आसपास के 3 रखता है।

सिग्नल

reactivity के लिए core एक हल्का signal सिस्टम इस्तेमाल करता है:

typescript
import { createSignal, createComputed, reaction } from '@reelkit/core';

// Create a signal
const count = createSignal(0);

// Observe changes (returns a disposer function)
const dispose = count.observe(() => console.log(count.value));

// Update value
count.value = 5;

// Create computed signal (requires a deps factory)
const doubled = createComputed(() => count.value * 2, () => [count]);

// Run side effects on signal changes
const disposeReaction = reaction(
  () => [count],
  () => console.log('Count changed:', count.value)
);

// Cleanup
dispose();
disposeReaction();

कंट्रोलर की स्टेट

reactive स्टेट controller.state से पढ़ें:

typescript
const { index, axisValue, indexes } = controller.state;

// Observe index changes (returns a disposer function)
const disposeIndex = index.observe(() => {
  console.log('Current index:', index.value);
});

// Observe visible indexes for virtualization
const disposeIndexes = indexes.observe(() => {
  console.log('Visible:', indexes.value);
});

// Cleanup when done
disposeIndex();
disposeIndexes();

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

किसी भी <video> एलिमेंट के लिए अपना scrub bar बनाएँ। कंट्रोलर अवधि, मौजूदा समय, buffered ranges और scrubbing स्टेट के reactive signals देता है, और एक ही कॉल में किसी भी DOM एलिमेंट पर pointer और कीबोर्ड इंटरैक्शन जोड़ देता है।

typescript
import { createTimelineController } from '@reelkit/core';

const timeline = createTimelineController({
  onScrubStart: () => video.pause(),
  onScrubEnd: () => video.play(),
});

timeline.attach(video);
const dispose = timeline.bindInteractions(trackEl);

// Render: read signals and update DOM
timeline.progress.observe(() => {
  fillEl.style.width = `${timeline.progress.value * 100}%`;
});

// Cleanup
dispose();
timeline.detach();

URL स्टेट

Overlay के खुले होने की स्टेट address bar में रखें: दिखने वाली स्लाइड को एक ऐसा लिंक मिलता है जिसे शेयर किया जा सके, जिस पर सीधे पहुँचा जा सके और जिसे back बटन से बंद किया जा सके। मॉडल core के पास है; bindings इसे एक hook (React/Vue में useOverlayUrlState, Angular में createOverlayUrlState) और URL से चलने वाले overlay कंपोनेंट में लपेटती हैं।

यह कैसे काम करता है

createUrlStateController एक query parameter को signal में दर्शाता है और बदलाव वापस लिखता है। खोलने पर history में एक entry जुड़ती है; हर नेविगेशन उसी को बदल देता है — सौ स्वाइप भी कोई नई entry नहीं जोड़ते, इसलिए back का एक कदम हमेशा बंद कर देता है। UrlAdapter पढ़ने/लिखने की बदली जा सकने वाली कड़ी है: डिफ़ॉल्ट history.pushState चलाता है, और routing वाला ऐप router पर आधारित adapter देता है ताकि router की अपनी location कभी पुरानी न पड़े।

typescript
import { createUrlStateController, urlIndexKey } from '@reelkit/core';

const controller = createUrlStateController({
  param: 'photo',
  ...urlIndexKey(() => items.length),
});

const detach = controller.attach(); // begin mirroring the URL
controller.position.observe(() => {
  // null → closed; a number → open at that slide
  render(controller.position.value);
});

// Write back: opening pushes once, navigating replaces, closing clears
controller.set(3);
controller.set(null);

Codec और locator — दो अलग काम

एक key, { codec, locator } की मेल खाती जोड़ी है। किसी पहचान को URL में लिखना और यह ढूँढना कि वह अभी कहाँ है, दो अलग ज़िम्मेदारियाँ हैं, इसलिए ये दो अलग objects हैं:

  • codec — wire का काम। encode पहचान को parameter के टेक्स्ट में लिखता है; decode उसे वापस पढ़ता है, और गलत value को ठुकरा देता है ताकि parameter URL से अपने-आप हट जाए।
  • locator — खोज का काम। locate ढूँढता है कि decode की गई पहचान मौजूदा collection में कहाँ है (या हट चुकी हो तो null); identify लिखने के लिए किसी स्थिति को वापस उसकी पहचान में बदलता है; वैकल्पिक locateAsync न मिलने पर windowed या अनंत फ़ीड के अगले पेज लोड करता है।

इन्हें अलग रखने से आप किसी भी wire को किसी भी खोज के साथ जोड़ सकते हैं — जैसे स्थिर id वाला codec, पेज लोड करने वाले locator के साथ।

Index बनाम स्थिर id वाली keys

दो बिल्ट-इन keys आपके लिए यह जोड़ी बना देती हैं; फ़र्क सिर्फ़ इतना है कि URL किसका नाम लेता है:

  • urlIndexKey(() => count) स्थिति से पहचानता है (?photo=3)। सबसे आसान, लेकिन सूची का क्रम बदलते ही bookmark कोई दूसरा आइटम खोलता है।
  • urlStableIdKey({ items }) हर आइटम की स्थिर id से पहचानता है (?photo=post_42) और मौजूदा सूची में ढूँढता है — क्रम बदलने के बाद भी bookmark उसी आइटम का नाम लेता है, या आइटम हट जाने पर साफ़ तौर पर हट जाता है। hashCodec: base64UrlCodec id को base64url से छिपा देता है (वापस पढ़ा जा सकता है, cryptographic hash नहीं)।

Windowed फ़ीड के पेज लोड करने हैं? दोनों बिल्ट-इन keys वैकल्पिक locateAsync लेती हैं — urlIndexKey(() => count, locateAsync) और urlStableIdKey({ items, locateAsync })। Synchronous खोज लोड हो चुके आइटम के लिए जवाब देती है; न मिलने पर बाकी पेज लोड होते हैं, इसलिए विंडो से आगे का शेयर किया गया लिंक भी खुलता है — अपना codec या locator लिखने की ज़रूरत नहीं।

दो अक्ष? urlIndexTwoAxisKey किसी पोस्ट और उसके अंदर के मीडिया index के लिए ?p=<outer>.<inner> रखता है। सारे विकल्प Core API रेफ़रेंस में हैं।

देखे गए आइटम की स्टेट

याद रखें कि दर्शक गैलरी में कहाँ तक पहुँचा, reload और tabs के पार भी — एक ring जो दिखाती है कि क्या देखा जा चुका है, एक गैलरी जो वहीं से खुलती है जहाँ छोड़ी गई थी। यह URL स्टेट वाला ही मॉडल है, बस address bar की जगह storage की ओर इशारा करता है, इसलिए दोनों एक ही key साझा करते हैं।

यह कैसे काम करता है

createViewedStateController एक entry को ठीक उसी टेक्स्ट के रूप में सहेजता है जो ?photo= लिंक में होता, और उसे उसी decode फिर locate चक्र से वापस पढ़ता है। सहेजी गई स्थिति पर कभी सीधे भरोसा नहीं किया जाता। एक ही key दोनों जगह spread करें, तो bookmark और सहेजी गई entry एक ही string होते हैं।

typescript
import {
  createUrlStateController,
  createViewedStateController,
  urlStableIdTwoAxisKey,
  twoAxisViewedTracking,
} from '@reelkit/core';

const key = urlStableIdTwoAxisKey({ outerItems, innerItems });

const url = createUrlStateController({ param: 'story', ...key });
const seen = createViewedStateController({
  storageKey: 'stories-seen',
  ...key,
  ...twoAxisViewedTracking,
});

seen.attach();                       // reads storage, follows other tabs
seen.record({ outer: 2, inner: 1 }); // furthest point wins, a rewatch never rewinds
seen.resolve('user_42');             // → { outer: 2, inner: 1 } | null

टिकाऊपन key पर निर्भर है

Store अपनी तरफ़ से कोई मरम्मत नहीं करता, इसलिए आप जो key spread करते हैं वही तय करती है कि क्या बचा रहेगा: id से पहचानने वाली key collection का क्रम बदलने पर भी जगह बनाए रखती है, स्थिति से पहचानने वाली नहीं। सहेजी गई entry देखे जाने की गिनती नहीं, बल्कि सबसे दूर पहुँचे बिंदु का नाम है, इसलिए बीच से कोई आइटम हटाने पर गिनती घट जाती है — वैसे ही जैसे शेयर किया गया लिंक खुद को ठीक कर लेता है।

पढ़ना सिर्फ़ synchronous है। जिस entry के आइटम अभी लोड नहीं हुए, वह मौजूद नहीं मानी जाती और storage में बिना छुए पड़ी रहती है, इसलिए windowed फ़ीड अपनी ही history कभी नहीं मिटाती।

Storage और समय-सीमा

डिफ़ॉल्ट रूप से store localStorage पर चलता है; createSessionStorageAdapter() tab बंद होने पर भूल जाता है, और आपका अपना StorageAdapter इसे किसी भी synchronous जगह रख सकता है। attach() से पहले कुछ भी नहीं पढ़ा जाता, इसलिए सर्वर रेंडर और क्लाइंट का पहला रेंडर एक जैसे रहते हैं।

Entries तब तक रखी जाती हैं जब तक उन्हें साफ़ तौर पर भुलाया न जाए। उन्हें समाप्त करने के लिए ttlMs पास करें — हर track के लिए अलग, खिसकती घड़ी पर, ताकि जो चीज़ अभी भी देखी जा रही है वह छोड़ी गई चीज़ के साथ पुरानी न पड़े। इससे लिखा जाने वाला रूप बदलता है — हर entry [wire, timestamp] जोड़ी बन जाती है — लेकिन विकल्प कुछ भी हो, पढ़ना दोनों रूप संभाल लेता है, इसलिए विकल्प चालू करने से पहले सहेजी गई entry मिटाई नहीं जाती बल्कि ताज़ा मानी जाती है। maxTracks उम्र की जगह गिनती सीमित करता है: सीमा पार होने पर अगली बार लिखते समय सबसे पहले दर्ज किया गया track हटा दिया जाता है, और किसी track को दर्ज करना — भले ही स्थिति पीछे की हो — उसे कतार के आख़िर में भेज देता है। सारे विकल्प Core API रेफ़रेंस में हैं।

ऑटोप्ले

हर उस चीज़ की टाइमिंग जो अपने आप आगे बढ़ती है। Stories Player हर story को createTimerController पर चलाता है; Lightbox अपना स्लाइडशो createSlideshowController पर चलाते हैं, जो उसी पर बना है। दोनों requestAnimationFrame पर चलते हैं, और हर pause सटीक प्रगति बचाए रखता है।

createTimerController

एक अवधि को 0 से 1 तक गिनता है और पूरा होने पर callback बुलाता है। start() हमेशा 0 से शुरू होता है, चाहें तो उस बार के लिए दूसरी अवधि के साथ; pause() और resume() सटीक स्थिति बचाए रखते हैं।

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

const timer = createTimerController({
  duration: 5000,
  onComplete: () => console.log('Timer finished!'),
});

// Observe progress (0 to 1)
const dispose = reaction(
  () => [timer.progress],
  () => {
    console.log('Progress:', timer.progress.value);
  },
);

// Start with default duration
timer.start();

// Or override duration for a specific story
timer.start(8000);

// Pause/resume preserves exact position
timer.pause();
timer.resume();

// Reset to 0
timer.reset();

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

createSlideshowController

अपने आप आगे बढ़ने वाली गैलरी के पीछे की state machine। यह तभी चलती है जब चालू हो, कोई इसे रोके न हो और पेज दिख रहा हो; पेज के दिखने पर यह खुद नज़र रखती है, बाकी आप बताते हैं। इमेज अपना interval लोड होने के पल से गिनती है, इसलिए धीमी इमेज बिना देखे कभी नहीं छूटती। वीडियो खत्म होने पर आगे बढ़ता है, इसलिए उसे loop की जगह एक बार चलना चाहिए। जो वीडियो स्लाइडशो के रुके रहने के दौरान खत्म हो, वह रोक हटने पर आगे बढ़ता है।

typescript
import { createSlideshowController, createSliderController } from '@reelkit/core';

const items = [{ type: 'image' }, { type: 'video' }, { type: 'image' }];

const slider = createSliderController(
  { count: items.length, direction: 'horizontal' },
  { onAfterChange: (index) => onSlideChange(index) },
);

const slideshow = createSlideshowController({
  interval: 5000,
  onAdvance: () => {
    if (slider.state.index.value >= items.length - 1) slideshow.pause();
    else slider.next();
  },
});

// Every slide change starts over and waits for the new slide.
function onSlideChange(index: number) {
  slideshow.setSlide(items[index].type === 'video' ? 'video' : 'image');
}

// From the slide: an image counts its interval once it loaded or failed,
// a video advances when it ends (let it play once rather than loop).
const onImageSettled = () => slideshow.ready();
const onVideoEnded = () => slideshow.ended();

// Holds stack: the slideshow runs again once every reason is released.
const onControlsEnter = () => slideshow.hold('hover');
const onControlsLeave = () => slideshow.release('hover');

onSlideChange(0);
slideshow.play();

// Later
slideshow.dispose();

पूरा config, स्टेट और methods Core API रेफ़रेंस में हैं।

डाउनलोड

downloadFile(url, filename?) फ़ाइल को filename नाम से सहेजता है, जो डिफ़ॉल्ट रूप से URL के path का आख़िरी हिस्सा है। दूसरे origin के लिंक पर download attribute अनदेखा हो जाता है, इसलिए यह फ़ाइल को fetch करके blob सहेजता है। जब fetch विफल हो या सर्वर error लौटाए — आम तौर पर बिना CORS headers वाला दूसरे origin का सर्वर — तो URL नए टैब में खुलता है, जिससे पाठक को फ़ाइल फिर भी मिल जाती है। यह कभी reject नहीं होता, और window के बिना कुछ नहीं करता। देखें Core API रेफ़रेंस।

अगले कदम