@streamscloud/kit 0.0.1-1771006168898 → 0.0.1-1771072688121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/ui/button/resources/button-theme.svelte +8 -11
  2. package/dist/ui/dialog/cmp.dialog-container.svelte +1 -1
  3. package/dist/ui/player/buttons/cmp.mobile-player-buttons.svelte +37 -0
  4. package/dist/ui/player/buttons/cmp.mobile-player-buttons.svelte.d.ts +7 -0
  5. package/dist/ui/player/buttons/cmp.player-buttons.svelte +127 -0
  6. package/dist/ui/player/buttons/cmp.player-buttons.svelte.d.ts +9 -0
  7. package/dist/ui/player/buttons/cmp.responsive-player-buttons.svelte +32 -0
  8. package/dist/ui/player/buttons/cmp.responsive-player-buttons.svelte.d.ts +8 -0
  9. package/dist/ui/player/buttons/index.d.ts +4 -0
  10. package/dist/ui/player/buttons/index.js +3 -0
  11. package/dist/ui/player/buttons/types.d.ts +7 -0
  12. package/dist/ui/player/buttons/types.js +1 -0
  13. package/dist/ui/player/carousel/carousel-localization.d.ts +3 -0
  14. package/dist/ui/player/carousel/carousel-localization.js +12 -0
  15. package/dist/ui/player/carousel/cmp.carousel.svelte +418 -0
  16. package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +38 -0
  17. package/dist/ui/player/carousel/index.d.ts +2 -0
  18. package/dist/ui/player/carousel/index.js +1 -0
  19. package/dist/ui/player/carousel/types.d.ts +1 -0
  20. package/dist/ui/player/carousel/types.js +1 -0
  21. package/dist/ui/player/feed-slider/cmp.feed-slider.svelte +206 -0
  22. package/dist/ui/player/feed-slider/cmp.feed-slider.svelte.d.ts +43 -0
  23. package/dist/ui/player/feed-slider/index.d.ts +3 -0
  24. package/dist/ui/player/feed-slider/index.js +2 -0
  25. package/dist/ui/player/feed-slider/prevent-feed-scroll.d.ts +5 -0
  26. package/dist/ui/player/feed-slider/prevent-feed-scroll.js +11 -0
  27. package/dist/ui/player/feed-slider/types.d.ts +16 -0
  28. package/dist/ui/player/feed-slider/types.js +1 -0
  29. package/dist/ui/player/feed-slider/wheel-gestures-adapter.d.ts +17 -0
  30. package/dist/ui/player/feed-slider/wheel-gestures-adapter.js +79 -0
  31. package/dist/ui/player/providers/chunks-player-buffer/index.d.ts +2 -0
  32. package/dist/ui/player/providers/chunks-player-buffer/index.js +2 -0
  33. package/dist/ui/player/providers/chunks-player-buffer/player-chunk-item.svelte.d.ts +9 -0
  34. package/dist/ui/player/providers/chunks-player-buffer/player-chunk-item.svelte.js +9 -0
  35. package/dist/ui/player/providers/chunks-player-buffer/player-chunk.svelte.d.ts +30 -0
  36. package/dist/ui/player/providers/chunks-player-buffer/player-chunk.svelte.js +72 -0
  37. package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.d.ts +23 -0
  38. package/dist/ui/player/providers/chunks-player-buffer/player-chunks-manager.svelte.js +220 -0
  39. package/dist/ui/player/providers/default-chunks-player-buffer.svelte.d.ts +23 -0
  40. package/dist/ui/player/providers/default-chunks-player-buffer.svelte.js +71 -0
  41. package/dist/ui/player/providers/default-feed-player-buffer.svelte.d.ts +29 -0
  42. package/dist/ui/player/providers/default-feed-player-buffer.svelte.js +121 -0
  43. package/dist/ui/player/providers/index.d.ts +4 -0
  44. package/dist/ui/player/providers/index.js +3 -0
  45. package/dist/ui/player/providers/service.d.ts +2 -0
  46. package/dist/ui/player/providers/service.js +13 -0
  47. package/dist/ui/player/providers/types.d.ts +54 -0
  48. package/dist/ui/player/providers/types.js +1 -0
  49. package/dist/ui/player/utils/index.d.ts +1 -0
  50. package/dist/ui/player/utils/index.js +1 -0
  51. package/dist/ui/player/utils/touch-synchronizer.d.ts +7 -0
  52. package/dist/ui/player/utils/touch-synchronizer.js +21 -0
  53. package/dist/ui/progress/cmp.progress.svelte +1 -1
  54. package/package.json +19 -1
@@ -0,0 +1,418 @@
1
+ <script lang="ts" generics="T">import { Utils, isBrowser } from '../../../core/utils';
2
+ import { Icon } from '../../icon';
3
+ import { TouchSynchronizer } from '../utils';
4
+ import { CarouselLocalization } from './carousel-localization';
5
+ import IconChevronLeft from '@fluentui/svg-icons/icons/chevron_left_20_regular.svg?raw';
6
+ import IconChevronRight from '@fluentui/svg-icons/icons/chevron_right_20_regular.svg?raw';
7
+ import { onDestroy, onMount, untrack } from 'svelte';
8
+ let { items, mode = 'arrows-with-counts', initialIndex, autoSlideMs = 0, on, dot, children } = $props();
9
+ const localization = new CarouselLocalization();
10
+ const itemIndices = $derived(items.map((_, index) => index));
11
+ const animationDuration = 300;
12
+ let previousItems = $state.raw(undefined);
13
+ let selectedIndex = $state(untrack(() => initialIndex));
14
+ let animationIndex = $state(null);
15
+ let interval;
16
+ let slidesRef;
17
+ let sliderWidth = $state(0);
18
+ let swipeTransition = $state(0);
19
+ let resizeObserver;
20
+ onMount(() => {
21
+ notifyIndexChanged();
22
+ setTimeout(() => {
23
+ slidesRef?.classList.toggle('animate', false);
24
+ });
25
+ let touchStartX = 0;
26
+ let touchStartY = 0;
27
+ let touchMoveX = 0;
28
+ let touchStartTime = 0;
29
+ let swipeState = 'not-initialized';
30
+ slidesRef.addEventListener('touchstart', (e) => {
31
+ TouchSynchronizer.touchStarted(slidesRef);
32
+ slidesRef.classList.toggle('animate', false);
33
+ touchStartX = e.changedTouches[0].screenX;
34
+ touchStartY = e.changedTouches[0].screenY;
35
+ touchStartTime = Date.now();
36
+ swipeState = 'not-initialized';
37
+ }, { passive: true });
38
+ slidesRef.addEventListener('touchmove', (e) => {
39
+ if (swipeState === 'horizontal') {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
43
+ }
44
+ else if (swipeState === 'vertical') {
45
+ return;
46
+ }
47
+ const touchCurrentX = e.changedTouches[0].screenX;
48
+ const touchCurrentY = e.changedTouches[0].screenY;
49
+ const diffX = Math.abs(touchCurrentX - touchStartX);
50
+ const diffY = Math.abs(touchCurrentY - touchStartY);
51
+ // Determine direction on first significant movement
52
+ if (diffX > 10 || diffY > 10) {
53
+ if (diffX > diffY) {
54
+ // Horizontal swipe - block vertical scroll
55
+ swipeState = 'horizontal';
56
+ e.preventDefault();
57
+ e.stopPropagation();
58
+ TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
59
+ touchMoveX = touchCurrentX - touchStartX;
60
+ const basePosition = -sliderWidth * (animationIndex === null ? selectedIndex + 1 : animationIndex);
61
+ swipeTransition = basePosition + touchMoveX;
62
+ }
63
+ else {
64
+ // Vertical swipe - allow parent to handle
65
+ swipeState = 'vertical';
66
+ }
67
+ }
68
+ }, { passive: false });
69
+ slidesRef.addEventListener('touchend', (e) => {
70
+ const reset = () => {
71
+ swipeTransition = 0;
72
+ touchMoveX = 0;
73
+ };
74
+ if (swipeState !== 'horizontal') {
75
+ reset();
76
+ return;
77
+ }
78
+ // This is a horizontal swipe - block propagation
79
+ e.stopPropagation();
80
+ e.preventDefault();
81
+ slidesRef.classList.toggle('animate', true);
82
+ TouchSynchronizer.touchEndPropatationBlocked(slidesRef);
83
+ TouchSynchronizer.touchEnded(slidesRef);
84
+ const isFastSwipe = Date.now() - touchStartTime < 300;
85
+ if (!isFastSwipe && Math.abs(touchMoveX) < sliderWidth / 6) {
86
+ reset();
87
+ return;
88
+ }
89
+ if (touchMoveX < 0) {
90
+ loadNext();
91
+ }
92
+ else {
93
+ loadPrevious();
94
+ }
95
+ reset();
96
+ }, { passive: false });
97
+ slidesRef.addEventListener('transitionend', (e) => {
98
+ e.stopPropagation();
99
+ if (animationIndex !== null && animationIndex > items.length - 1) {
100
+ slidesRef.classList.toggle('animate', false);
101
+ animationIndex = null;
102
+ }
103
+ if (animationIndex !== null && animationIndex === 0) {
104
+ slidesRef.classList.toggle('animate', false);
105
+ animationIndex = null;
106
+ }
107
+ setTimeout(() => {
108
+ slidesRef?.classList.toggle('animate', true);
109
+ });
110
+ });
111
+ if (autoSlideMs) {
112
+ interval = window.setInterval(() => {
113
+ if (items?.length <= 1) {
114
+ return;
115
+ }
116
+ loadNext();
117
+ }, autoSlideMs);
118
+ }
119
+ sliderWidth = slidesRef.getBoundingClientRect().width;
120
+ let resizeTimeout = 0;
121
+ resizeObserver = new ResizeObserver(() => {
122
+ if (isBrowser() && resizeTimeout) {
123
+ window.clearTimeout(resizeTimeout);
124
+ }
125
+ slidesRef.classList.toggle('animate', false);
126
+ sliderWidth = slidesRef.getBoundingClientRect().width;
127
+ resizeTimeout = window.setTimeout(() => {
128
+ slidesRef?.classList.toggle('animate', true);
129
+ });
130
+ });
131
+ resizeObserver.observe(slidesRef);
132
+ if (isBrowser()) {
133
+ window.addEventListener(`keydown`, onKeyPress);
134
+ }
135
+ });
136
+ onDestroy(() => {
137
+ if (resizeObserver) {
138
+ resizeObserver.disconnect();
139
+ }
140
+ if (isBrowser()) {
141
+ window.removeEventListener('keydown', onKeyPress);
142
+ if (interval) {
143
+ window.clearInterval(interval);
144
+ }
145
+ }
146
+ });
147
+ const onKeyPress = Utils.throttle((e) => {
148
+ if (e.key === 'ArrowLeft') {
149
+ loadPrevious();
150
+ }
151
+ if (e.key === 'ArrowRight') {
152
+ loadNext();
153
+ }
154
+ }, animationDuration);
155
+ const notifyIndexChanged = () => {
156
+ on?.indexChanged?.(selectedIndex);
157
+ };
158
+ notifyIndexChanged();
159
+ const setIndex = (index) => {
160
+ if (index < 0) {
161
+ animationIndex = 0;
162
+ selectedIndex = items.length - 1;
163
+ }
164
+ else if (index > items.length - 1) {
165
+ animationIndex = index + 1;
166
+ selectedIndex = 0;
167
+ }
168
+ else {
169
+ animationIndex = null;
170
+ selectedIndex = index;
171
+ }
172
+ setTimeout(() => {
173
+ notifyIndexChanged();
174
+ }, 600);
175
+ };
176
+ const loadPrevious = Utils.throttle(() => {
177
+ setIndex(--selectedIndex);
178
+ }, animationDuration);
179
+ const loadNext = Utils.throttle(() => {
180
+ setIndex(++selectedIndex);
181
+ }, animationDuration);
182
+ $effect(() => {
183
+ if (previousItems && items !== previousItems) {
184
+ setIndex(0);
185
+ }
186
+ previousItems = items;
187
+ });
188
+ const slidesTranslateX = $derived(swipeTransition || (items.length > 1 ? -sliderWidth * (animationIndex === null ? selectedIndex + 1 : animationIndex) : 0));
189
+ </script>
190
+
191
+ <div
192
+ class="carousel"
193
+ class:carousel--arrows-and-dots={mode === 'arrows-and-dots'}
194
+ class:carousel--arrows-only={mode === 'arrows-only'}
195
+ class:carousel--dots-only={mode === 'dots-only'}
196
+ class:carousel--dots-only-below={mode === 'dots-only-below'}
197
+ class:carousel--arrows-with-counts={mode === 'arrows-with-counts'}
198
+ style:--carousel--animation="{animationDuration}ms ease-out transform">
199
+ <div class="carousel__slides" bind:this={slidesRef} style:transform="translateX({slidesTranslateX}px)">
200
+ {#if items.length > 1}
201
+ <div class="carousel__slide">
202
+ {#if children}
203
+ {@render children(items[items.length - 1])}
204
+ {/if}
205
+ </div>
206
+ {/if}
207
+ {#each items as item (item)}
208
+ <div class="carousel__slide">
209
+ {#if children}
210
+ {@render children(item)}
211
+ {/if}
212
+ </div>
213
+ {/each}
214
+ {#if items.length > 1}
215
+ <div class="carousel__slide" data-after>
216
+ {#if children}
217
+ {@render children(items[0])}
218
+ {/if}
219
+ </div>
220
+ {/if}
221
+ </div>
222
+
223
+ {#if items.length > 1}
224
+ {#if mode === 'arrows-with-counts'}
225
+ <div class="carousel__arrows-with-counts">
226
+ <button type="button" class="carousel__counts-navigation-button" onclick={loadPrevious} aria-label="none">
227
+ <Icon src={IconChevronLeft} />
228
+ </button>
229
+ <div class="carousel__counts">{localization.nOfM(selectedIndex + 1, items.length)}</div>
230
+ <button type="button" class="carousel__counts-navigation-button" onclick={loadNext} aria-label="none">
231
+ <Icon src={IconChevronRight} />
232
+ </button>
233
+ </div>
234
+ {:else}
235
+ <button type="button" class="carousel__navigation-button carousel__navigation-button--prev" onclick={loadPrevious} aria-label="none">
236
+ <Icon src={IconChevronLeft} />
237
+ </button>
238
+ <button type="button" class="carousel__navigation-button carousel__navigation-button--next" onclick={loadNext} aria-label="none">
239
+ <Icon src={IconChevronRight} />
240
+ </button>
241
+ <div class="carousel__dots">
242
+ {#each itemIndices as index (index)}
243
+ <button type="button" class="carousel__dot" onclick={() => setIndex(index)}>
244
+ {#if dot}
245
+ {@render dot({ active: index === selectedIndex })}
246
+ {:else}
247
+ <span class="carousel__dot-indicator" class:carousel__dot-indicator--active={index === selectedIndex}></span>
248
+ {/if}
249
+ </button>
250
+ {/each}
251
+ </div>
252
+ {/if}
253
+ {/if}
254
+ </div>
255
+
256
+ <style>@charset "UTF-8";
257
+ .carousel {
258
+ /* Public API */
259
+ --_carousel--button-color: var(--sc-kit--carousel--button-color, #9ca3af);
260
+ --_carousel--dot-color: var(--sc-kit--carousel--dot-color, #ffffff);
261
+ --_carousel--dot-size: var(--sc-kit--carousel--dot-size, 0.5rem);
262
+ --_carousel--text-color: var(--sc-kit--carousel--text-color, #ffffff);
263
+ --_carousel--animation: var(--carousel--animation);
264
+ /* Mode control — overridden by modifiers */
265
+ --_carousel--nav-display: block;
266
+ --_carousel--nav-opacity: 0;
267
+ --_carousel--nav-visibility: hidden;
268
+ --_carousel--dots-display: flex;
269
+ --_carousel--dots-position: absolute;
270
+ --_carousel--dots-margin-top: 0;
271
+ display: flex;
272
+ flex-direction: column;
273
+ flex: 1;
274
+ position: relative;
275
+ width: 100%;
276
+ height: 100%;
277
+ overflow: hidden;
278
+ }
279
+ .carousel:hover {
280
+ --_carousel--nav-opacity: 1;
281
+ --_carousel--nav-visibility: visible;
282
+ }
283
+ .carousel--arrows-only {
284
+ --_carousel--dots-display: none;
285
+ }
286
+ .carousel--dots-only {
287
+ --_carousel--nav-display: none;
288
+ }
289
+ .carousel--dots-only-below {
290
+ --_carousel--nav-display: none;
291
+ --_carousel--dots-position: static;
292
+ --_carousel--dots-margin-top: 1rem;
293
+ }
294
+ .carousel__slides {
295
+ display: flex;
296
+ height: 100%;
297
+ }
298
+ .carousel__slides:global(.animate) {
299
+ transition: var(--_carousel--animation);
300
+ }
301
+ .carousel__slide {
302
+ display: flex;
303
+ justify-content: center;
304
+ align-items: center;
305
+ position: relative;
306
+ height: 100%;
307
+ width: 100%;
308
+ min-width: 100%;
309
+ max-width: 100%;
310
+ }
311
+ .carousel__dots {
312
+ margin: 0;
313
+ display: var(--_carousel--dots-display);
314
+ justify-content: center;
315
+ z-index: 1;
316
+ gap: 0.5rem;
317
+ flex-wrap: wrap;
318
+ position: var(--_carousel--dots-position);
319
+ margin-top: var(--_carousel--dots-margin-top);
320
+ left: 0;
321
+ right: 0;
322
+ bottom: 0.625rem;
323
+ }
324
+ .carousel__dot {
325
+ cursor: pointer;
326
+ padding: 0;
327
+ background: transparent;
328
+ border: none;
329
+ }
330
+ .carousel__dot-indicator {
331
+ display: block;
332
+ width: var(--_carousel--dot-size);
333
+ height: var(--_carousel--dot-size);
334
+ border-radius: 50%;
335
+ border: 1px solid var(--_carousel--dot-color);
336
+ background: transparent;
337
+ transition: background-color 0.2s;
338
+ }
339
+ .carousel__dot-indicator--active {
340
+ background: var(--_carousel--dot-color);
341
+ }
342
+ .carousel__navigation-button {
343
+ display: var(--_carousel--nav-display);
344
+ font-size: 0.75rem;
345
+ width: 1.875rem;
346
+ height: 1.875rem;
347
+ background-color: var(--_carousel--button-color);
348
+ border-radius: 50%;
349
+ text-align: center;
350
+ color: var(--_carousel--text-color);
351
+ position: absolute;
352
+ top: 50%;
353
+ transform: translateY(-50%);
354
+ opacity: var(--_carousel--nav-opacity);
355
+ visibility: var(--_carousel--nav-visibility);
356
+ transition: opacity 0.2s, visibility 0.2s;
357
+ /* Set 'container-type: inline-size;' to reference container*/
358
+ }
359
+ @container (width < 576px) {
360
+ .carousel__navigation-button {
361
+ opacity: 1;
362
+ visibility: visible;
363
+ }
364
+ }
365
+ .carousel__navigation-button:disabled {
366
+ opacity: 0.5;
367
+ cursor: default;
368
+ }
369
+ .carousel__navigation-button--prev {
370
+ left: 0.9375rem;
371
+ }
372
+ .carousel__navigation-button--next {
373
+ right: 0.9375rem;
374
+ }
375
+ .carousel__arrows-with-counts {
376
+ position: absolute;
377
+ bottom: 0.75rem;
378
+ left: 50%;
379
+ transform: translateX(-50%);
380
+ display: flex;
381
+ justify-content: center;
382
+ align-items: center;
383
+ height: 1.625rem;
384
+ z-index: 1;
385
+ }
386
+ .carousel__counts {
387
+ font-size: 0.9375rem;
388
+ font-weight: 300;
389
+ width: 5.625rem;
390
+ text-align: center;
391
+ color: var(--_carousel--text-color);
392
+ text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 6px rgba(0, 0, 0, 0.05);
393
+ }
394
+ .carousel__counts-navigation-button {
395
+ font-size: 0.75rem;
396
+ width: 1.625rem;
397
+ height: 1.625rem;
398
+ background-color: var(--_carousel--button-color);
399
+ border-radius: 50%;
400
+ text-align: center;
401
+ color: var(--_carousel--text-color);
402
+ opacity: var(--_carousel--nav-opacity);
403
+ visibility: var(--_carousel--nav-visibility);
404
+ transition: opacity 0.2s, visibility 0.2s;
405
+ }
406
+ .carousel__counts-navigation-button:disabled {
407
+ opacity: 0.5;
408
+ cursor: default;
409
+ }
410
+ .carousel__counts-navigation-button {
411
+ /* Set 'container-type: inline-size;' to reference container*/
412
+ }
413
+ @container (width < 576px) {
414
+ .carousel__counts-navigation-button {
415
+ opacity: 1;
416
+ visibility: visible;
417
+ }
418
+ }</style>
@@ -0,0 +1,38 @@
1
+ import type { CarouselMode } from './types';
2
+ import { type Snippet } from 'svelte';
3
+ declare function $$render<T>(): {
4
+ props: {
5
+ items: T[];
6
+ initialIndex: number;
7
+ mode?: CarouselMode;
8
+ autoSlideMs?: number;
9
+ on?: {
10
+ indexChanged: (index: number) => void;
11
+ };
12
+ children: Snippet<[T]>;
13
+ dot?: Snippet<[{
14
+ active: boolean;
15
+ }]>;
16
+ };
17
+ exports: {};
18
+ bindings: "";
19
+ slots: {};
20
+ events: {};
21
+ };
22
+ declare class __sveltets_Render<T> {
23
+ props(): ReturnType<typeof $$render<T>>['props'];
24
+ events(): ReturnType<typeof $$render<T>>['events'];
25
+ slots(): ReturnType<typeof $$render<T>>['slots'];
26
+ bindings(): "";
27
+ exports(): {};
28
+ }
29
+ interface $$IsomorphicComponent {
30
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
31
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
32
+ } & ReturnType<__sveltets_Render<T>['exports']>;
33
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
34
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
35
+ }
36
+ declare const Cmp: $$IsomorphicComponent;
37
+ type Cmp<T> = InstanceType<typeof Cmp<T>>;
38
+ export default Cmp;
@@ -0,0 +1,2 @@
1
+ export { default as Carousel } from './cmp.carousel.svelte';
2
+ export type { CarouselMode } from './types';
@@ -0,0 +1 @@
1
+ export { default as Carousel } from './cmp.carousel.svelte';
@@ -0,0 +1 @@
1
+ export type CarouselMode = 'arrows-and-dots' | 'arrows-only' | 'dots-only' | 'dots-only-below' | 'arrows-with-counts';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,206 @@
1
+ <script lang="ts" generics="T extends { id: string }">import { TouchSynchronizer } from '../utils';
2
+ import { createWheelAdapter } from './wheel-gestures-adapter';
3
+ import { onDestroy, onMount, untrack } from 'svelte';
4
+ let { buffer, on, children } = $props();
5
+ let slidesRef;
6
+ let sliderHeight = $state(0);
7
+ let swipeTransition = $state(0);
8
+ let cssAnimationDuration = $derived(buffer.animationDuration * 0.55);
9
+ let resizeObserver;
10
+ let sliderIndex = $state.raw(untrack(() => buffer.currentIndex));
11
+ const actualTransition = $derived(-sliderHeight * sliderIndex);
12
+ const onKeyPress = (e) => {
13
+ if (e.key === 'ArrowUp' || e.key === 'PageUp') {
14
+ buffer.loadPrevious();
15
+ }
16
+ if (e.key === 'ArrowDown' || e.key === 'PageDown') {
17
+ buffer.loadNext();
18
+ }
19
+ };
20
+ $effect(() => {
21
+ if (buffer.currentIndex >= 0 && sliderIndex >= 0 && Math.abs(sliderIndex - buffer.currentIndex) === 1) {
22
+ slidesRef.classList.toggle('animate', true);
23
+ }
24
+ else if (buffer.currentIndex >= 0 && (Math.abs(sliderIndex - buffer.currentIndex) !== 0 || !active)) {
25
+ untrack(() => notifyOnItemChanged());
26
+ }
27
+ });
28
+ let active = null;
29
+ $effect(() => {
30
+ sliderIndex = buffer.currentIndex;
31
+ });
32
+ const notifyOnItemChanged = () => {
33
+ if (active && buffer.current?.id !== active.id) {
34
+ on?.itemDeactivated?.(active.id);
35
+ }
36
+ active = buffer.current;
37
+ if (active) {
38
+ on?.itemActivated?.(active.id);
39
+ }
40
+ };
41
+ onMount(() => {
42
+ let touchStartX = 0;
43
+ let touchStartY = 0;
44
+ let touchMoveY = 0;
45
+ let touchStartTime = 0;
46
+ let swipeState = 'not-initialized';
47
+ window.addEventListener(`keydown`, onKeyPress);
48
+ slidesRef.addEventListener('touchstart', (e) => {
49
+ TouchSynchronizer.touchStarted(slidesRef);
50
+ // The movement gets all janky if there's a transition on the elements.
51
+ slidesRef.classList.toggle('animate', false);
52
+ touchStartX = e.changedTouches[0].screenX;
53
+ touchStartY = e.changedTouches[0].screenY;
54
+ touchStartTime = Date.now();
55
+ swipeState = 'not-initialized';
56
+ }, { passive: true });
57
+ slidesRef.addEventListener('touchmove', (e) => {
58
+ if (swipeState === 'vertical') {
59
+ // Already determined this is a vertical swipe
60
+ e.preventDefault();
61
+ e.stopPropagation();
62
+ TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
63
+ }
64
+ else if (swipeState === 'horizontal') {
65
+ // Already determined this is a horizontal swipe
66
+ return;
67
+ }
68
+ const touchCurrentX = e.changedTouches[0].screenX;
69
+ const touchCurrentY = e.changedTouches[0].screenY;
70
+ const diffX = Math.abs(touchCurrentX - touchStartX);
71
+ const diffY = Math.abs(touchCurrentY - touchStartY);
72
+ // Determine direction on first significant movement
73
+ if (diffX > 10 || diffY > 10) {
74
+ if (diffY > diffX) {
75
+ // Vertical swipe - handle it
76
+ swipeState = 'vertical';
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ TouchSynchronizer.touchMovePropagationBlocked(slidesRef);
80
+ const newPosition = touchCurrentY;
81
+ const diff = newPosition - touchStartY;
82
+ if ((diff > 0 && buffer.canLoadPrevious) || (diff < 0 && buffer.canLoadNext)) {
83
+ touchMoveY = diff;
84
+ swipeTransition = diff + actualTransition;
85
+ }
86
+ }
87
+ else {
88
+ // Horizontal swipe - allow child to handle
89
+ swipeState = 'horizontal';
90
+ return;
91
+ }
92
+ }
93
+ }, { passive: false });
94
+ slidesRef.addEventListener('touchend', (e) => {
95
+ const reset = () => {
96
+ swipeTransition = 0;
97
+ touchMoveY = 0;
98
+ };
99
+ // Check if this is a vertical swipe
100
+ if (swipeState !== 'vertical') {
101
+ // Horizontal swipe - don't handle and don't block
102
+ reset();
103
+ return;
104
+ }
105
+ // This is a vertical swipe - block propagation
106
+ e.stopPropagation();
107
+ TouchSynchronizer.touchEndPropatationBlocked(slidesRef);
108
+ TouchSynchronizer.touchEnded(slidesRef);
109
+ slidesRef.classList.toggle('animate', true);
110
+ const isFastSwipe = Date.now() - touchStartTime < 300;
111
+ if (!touchMoveY || (!isFastSwipe && Math.abs(touchMoveY) < sliderHeight / 6)) {
112
+ return reset();
113
+ }
114
+ if (touchMoveY > 0) {
115
+ buffer.loadPrevious();
116
+ }
117
+ else {
118
+ buffer.loadNext();
119
+ }
120
+ reset();
121
+ });
122
+ slidesRef.addEventListener('transitionend', (e) => {
123
+ if (e.target !== slidesRef) {
124
+ return;
125
+ }
126
+ slidesRef.classList.toggle('animate', false);
127
+ notifyOnItemChanged();
128
+ });
129
+ sliderHeight = slidesRef.clientHeight;
130
+ resizeObserver = new ResizeObserver(() => {
131
+ sliderHeight = slidesRef.clientHeight;
132
+ });
133
+ resizeObserver.observe(slidesRef);
134
+ });
135
+ onDestroy(() => {
136
+ if (resizeObserver) {
137
+ resizeObserver.disconnect();
138
+ }
139
+ window.removeEventListener('keydown', onKeyPress);
140
+ });
141
+ const wheelCallbacks = {
142
+ canLoadNext: () => buffer.canLoadNext,
143
+ canLoadPrevious: () => buffer.canLoadPrevious,
144
+ onTrigger: (direction) => {
145
+ // direction: 1 -> next, -1 -> previous
146
+ if (direction > 0) {
147
+ buffer.loadNext();
148
+ }
149
+ else if (direction < 0) {
150
+ buffer.loadPrevious();
151
+ }
152
+ },
153
+ getAnimationDurationMs: () => buffer.animationDuration
154
+ };
155
+ </script>
156
+
157
+ <div class="feed-slider">
158
+ <div
159
+ class="feed-slider__slides"
160
+ bind:this={slidesRef}
161
+ use:createWheelAdapter={{ cbs: wheelCallbacks }}
162
+ style:transform="translateY({swipeTransition || actualTransition}px)"
163
+ style:--feed-slider--animation="{cssAnimationDuration}ms ease-out transform">
164
+ {#each buffer.loaded as item, index (item)}
165
+ <div class="feed-slider__slide">
166
+ {#if index >= sliderIndex - 1 && index <= sliderIndex + 1}
167
+ <!-- Only render the active slide and its immediate neighbors for performance -->
168
+ {@render children({ item, active: index === sliderIndex })}
169
+ {/if}
170
+ </div>
171
+ {/each}
172
+ </div>
173
+ </div>
174
+
175
+ <style>.feed-slider {
176
+ position: relative;
177
+ overflow: clip;
178
+ height: 100%;
179
+ width: 100%;
180
+ min-width: 100%;
181
+ max-width: 100%;
182
+ }
183
+ .feed-slider__slides {
184
+ --_feed-slider--animation: var(--feed-slider--animation);
185
+ height: 100%;
186
+ min-height: 100%;
187
+ max-height: 100%;
188
+ width: 100%;
189
+ min-width: 100%;
190
+ max-width: 100%;
191
+ }
192
+ .feed-slider__slides:global(.animate) {
193
+ transition: var(--_feed-slider--animation);
194
+ }
195
+ .feed-slider__slide {
196
+ display: flex;
197
+ justify-content: center;
198
+ align-items: center;
199
+ position: relative;
200
+ height: 100%;
201
+ min-height: 100%;
202
+ max-height: 100%;
203
+ width: 100%;
204
+ min-width: 100%;
205
+ max-width: 100%;
206
+ }</style>