Посібник з ядра
The @reelkit/core містить логіку слайдера без прив’язки до фреймворку. Беріть його для власних інтеграцій або щоб розібратися в архітектурі під капотом.
Огляд архітектури
Ядро побудоване на патерні контролерів із фабричними функціями. Жодних класів — лише звичайні об’єкти, повернуті із замикань. Нуль залежностей. Ядро узгоджує роботу:
- SliderController — центральний стан і навігація
- GestureController — обробка перетягування дотиком і вказівником
- KeyboardController — стрілки та Escape
- WheelController — колесо миші з тротлінгом
createSliderController
Створює новий екземпляр контролера слайдера, який керує всім станом і поведінкою.
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();Методи контролера
Навігація
// Go to specific index
controller.goTo(5); // instant
controller.goTo(5, true); // animated, returns Promise
// Navigate to next/previous
controller.next();
controller.prev();Життєвий цикл
// 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);Оновлення стану
// Update configuration
controller.updateConfig({
count: 20,
loop: true,
});Віртуалізація
Ядро тримає в DOM лише 3 слайди у будь-який момент (поточний, попередній, наступний). Екстрактор діапазону вирішує, які індекси потрапляють у вікно рендерингу:
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 навколо поточного слайда.
Сигнали
Для реактивності ядро використовує легку систему сигналів:
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();Стан контролера
Доступ до реактивного стану — через controller.state:
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> елемента. Контролер дає реактивні сигнали тривалості, поточного часу, буферизованих діапазонів і стану перемотування, а також одним викликом навішує обробку вказівника й клавіатури на будь-який елемент DOM.
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
Винесіть стан відкриття оверлея в адресний рядок: видимий слайд отримує посилання, яким можна поділитися, відкрити напряму й закрити кнопкою «назад». Модель живе в ядрі; прив’язки загортають її в хук (React / Vue useOverlayUrlState, Angular createOverlayUrlState) і компонент оверлея, керований URL.
Як це працює
createUrlStateController віддзеркалює один параметр запиту в сигнал і записує зміни назад. Відкриття додає один запис в історію; кожна навігація замінює його — сто свайпів не додають жодного, тож один крок назад завжди закриває. UrlAdapter — це змінний шов читання й запису: типовий керує history.pushState, а застосунок із роутером передає адаптер на базі роутера, щоб власне місцеположення роутера не застаріло.
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 } . Записати ідентичність в URL і знайти, де вона зараз, — різні задачі, тож і об’єкти різні:
- кодек — формат передавання.
encodeзаписує ідентичність у текст параметра;decodeрозбирає його назад і відхиляє зіпсоване значення, тож параметр сам зникає з URL. - локатор — пошук.
locateзнаходить, де розкодована ідентичність лежить у живій колекції (абоnullякщо її вже немає);identifyчитає позицію назад в ідентичність для запису; необов’язковийlocateAsyncдотягує сторінку вікна чи нескінченної стрічки, якщо не знайшлося.
Роздільність дає змогу поєднати будь-який формат із будь-яким пошуком — наприклад, кодек зі стабільним id і локатор із підвантаженням.
Ключі за індексом і за стабільним id
Два вбудовані ключі будують цю пару за вас; різняться лише тим, що саме називає URL:
urlIndexKey(() => count)адресує за позицією (?photo=3). Найпростіше, але після зміни порядку закладка відкриє інший елемент.urlStableIdKey({ items })адресує за стабільнимid(?photo=post_42), скануючи живий список — закладка називає той самий елемент і після зміни порядку, а якщо його немає, акуратно зникає.hashCodec: base64UrlCodecмаскує id у base64url (оборотно, це не криптографічний хеш).
Гортаєте стрічку вікнами? Обидва вбудовані ключі приймають необов’язковий locateAsync — urlIndexKey(() => count, locateAsync) та urlStableIdKey({ items, locateAsync }). Синхронний пошук відповідає за вже завантажене; якщо не знайшлося, решта підвантажується, тож надіслане посилання за межі вікна все одно відкриється — без власноруч написаних кодека й локатора.
Дві осі? urlIndexTwoAxisKey несе ?p=<outer>.<inner> для допису разом із індексом внутрішнього медіа. Повний перелік опцій — у довіднику API ядра.
Переглянуте
Запам’ятовуйте, як далеко глядач дійшов у галереї, — між перезавантаженнями та вкладками: кільце, що показує переглянуте, галерея, що відкривається там, де її залишили. Це та сама модель, що й стан в URL, лише спрямована на сховище замість адресного рядка, тож обидві ділять один ключ.
Як це працює
createViewedStateController зберігає запис як точно той текст, який ніс би ?photo= посилання, і читає його назад через той самий цикл decode → locate. Збереженій позиції ніхто не довіряє. Розгорніть один ключ в обидві поверхні — і закладка та збережений запис будуть одним рядком.
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Довговічність визначає ключ
Сховище не додає власного відновлення, тож який ключ ви розгорнете, те й переживе зміни: ключ за ідентифікатором зберігає місце попри перевпорядкування колекції, ключ за позицією — ні. Збережений запис називає найдальшу досягнуту точку, а не підрахунок переглядів, тож вилучення елемента із середини скорочує лічильник — те саме самовиправлення, яке отримує спільне посилання.
Читання лише синхронне. Запис, елементи якого ще не завантажено, читається як відсутній і лишається у сховищі недоторканим, тож віконна стрічка ніколи не з’їдає власну історію.
Сховище та термін дії
Типово сховище стоїть на localStorage; createSessionStorageAdapter() забуває при закритті вкладки, а власний StorageAdapter кладе його будь-куди синхронно. Нічого не читається до attach(), тож серверний рендер і перший клієнтський збігаються.
Записи зберігаються, доки їх явно не забути. Передайте ttlMs, щоб натомість вони закінчувалися — на трек, за ковзним годинником, тож те, що ще дивляться, не застаріває поруч із покинутим. Це змінює запис: кожен елемент стає парою [wire, timestamp], але читання приймає обидві форми незалежно від опції, тож запис, збережений до її ввімкнення, вважається свіжим, а не видаляється. maxTracks обмежує кількість, а не вік: понад ліміт найдавніше записаний трек вилучається під час наступного запису, а запис треку, навіть позиції позаду, переставляє його в кінець черги. Повний перелік опцій — у довіднику API ядра.
Автовідтворення
Відлік часу для всього, що перемикається саме. Stories Player веде кожну story на createTimerController; Lightbox веде слайдшоу на createSlideshowController, побудованому на ньому. Обидва працюють на requestAnimationFrame, і кожна пауза зберігає точний прогрес.
createTimerController
Відлічує одну тривалість від 0 до 1 і викликає колбек, коли закінчує. start() завжди починає з 0, за бажанням з іншою тривалістю для цього запуску; pause() і resume() зберігають точну позицію.
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
Машина станів за галереєю, що гортається сама. Вона працює, лише поки увімкн ена, ніщо її не тримає і сторінка видима; за видимістю сторінки вона стежить сама, решту передаєте ви. Зображення відлічує свій інтервал від моменту завантаження, тож повільне ніколи не пропускається непоміченим. Відео перемикається, коли закінчується, тож має програватися один раз, а не по колу. Відео, що закінчилося, поки щось тримає слайдшоу, перемикається після звільнення.
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();Повна конфігурація, стан і методи — у довіднику API core.
Завантаження
downloadFile(url, filename?) зберігає файл під ім’ям filename, за замовчуванням — останнім сегментом шляху URL. Посилання на інше джерело ігнорує атрибут download, тому функція завантажує файл через fetch і зберігає blob. Коли fetch не вдається або сервер відповідає помилкою — зазвичай це сервер з іншого джерела без заголовків CORS, — URL відкривається в новій вкладці, тож читач однаково отримує файл. Ніколи не відхиляє проміс і нічого не робить без вікна. Див. довідник API core.
Наступні кроки
- Довідник API ядра - усі доступні пропси
- Посібник для фреймворку - компоненти, демо та інтеграціяПосібник для фреймворку - компоненти, демо та інтеграціяПосібник для фреймворку - компоненти, демо та інтеграція
- Reel Player - відеоплеєр у стилі TikTok / ReelsReel Player - відеоплеєр у стилі TikTok / ReelsReel Player - відеоплеєр у стилі TikTok / Reels
- Lightbox - галерея зображень і відеоLightbox - галерея зображень і відеоLightbox - галерея зображень і відео
- Stories Player - переглядач історій у стилі InstagramStories Player - переглядач історій у стилі InstagramStories Player - переглядач історій у стилі Instagram