@streamscloud/embeddable 6.0.0 → 6.0.2

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.
@@ -0,0 +1 @@
1
+ export declare const runningInBrowser: () => boolean;
@@ -0,0 +1 @@
1
+ export const runningInBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -10,14 +10,16 @@
10
10
  import { Utils } from '../../core/utils';
11
11
  import { default as ShortVideosPlayerView } from '../../short-videos/short-videos-player/short-videos-player-view.svelte';
12
12
  import { default as StreamPlayer } from '../../streams/stream-player/stream-player.svelte';
13
- import { Icon } from '../../ui/icon';
13
+ import { Dropdown } from '../../ui/dropdown';
14
+ import { Icon, IconColor } from '../../ui/icon';
14
15
  import { Loading } from '../../ui/loading';
15
16
  import { MediaCenterLocalization } from './media-center-localization';
16
17
  import { default as Overview } from './overview.svelte';
17
18
  import { makeShortVideosProvider } from './short-video-resources-generator';
18
19
  import { MediaCenterMode } from './types';
20
+ import IconLineHorizontal3 from '@fluentui/svg-icons/icons/line_horizontal_3_20_regular.svg?raw';
19
21
  import IconTextColumnThree from '@fluentui/svg-icons/icons/text_column_three_20_regular.svg?raw';
20
- import { onMount } from 'svelte';
22
+ import { onDestroy, onMount } from 'svelte';
21
23
  import { fade } from 'svelte/transition';
22
24
  let { dataProvider, playerProps, localization: localizationInit = 'en' } = $props();
23
25
  const localization = $derived(new MediaCenterLocalization(localizationInit));
@@ -30,6 +32,7 @@ let headerHeight = $state(0);
30
32
  let shortVideoProps = $state.raw(playerProps.type === MediaCenterMode.ShortVideos ? playerProps.props : null);
31
33
  let streamProps = $state.raw(playerProps.type === MediaCenterMode.Stream ? playerProps.props : null);
32
34
  let overviewData = $state.raw(null);
35
+ let scrollResizeObserver = null;
33
36
  const categories = $derived.by(() => {
34
37
  if (!mediaCenterConfig) {
35
38
  return [];
@@ -58,7 +61,13 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
58
61
  mediaDataLoading = false;
59
62
  }
60
63
  }
64
+ scrollResizeObserver = new ResizeObserver(() => {
65
+ updateScrollShadows();
66
+ });
61
67
  }));
68
+ onDestroy(() => {
69
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.disconnect();
70
+ });
62
71
  const selectCategory = (categoryId) => {
63
72
  if (!dataProvider) {
64
73
  return;
@@ -139,21 +148,19 @@ const uniqueById = (arr) => {
139
148
  }
140
149
  return res;
141
150
  };
142
- let wrapperRef = null;
143
151
  let scrollRef = null;
144
152
  let scrollHasLeft = $state(false);
145
153
  let scrollHasRight = $state(false);
146
154
  const mounted = (node, callback) => {
147
- wrapperRef = node;
148
- requestAnimationFrame(() => {
155
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
156
+ const heightResizeObserver = new ResizeObserver(() => {
149
157
  headerHeight = node.clientHeight;
150
158
  callback({ height: headerHeight });
151
159
  });
152
- const ro = new ResizeObserver(updateScrollShadows);
153
- ro.observe(wrapperRef);
160
+ heightResizeObserver.observe(node);
154
161
  return {
155
162
  destroy: () => {
156
- ro.disconnect();
163
+ heightResizeObserver.disconnect();
157
164
  }
158
165
  };
159
166
  };
@@ -167,9 +174,7 @@ const updateScrollShadows = () => {
167
174
  };
168
175
  const onScrollMounted = (node) => {
169
176
  scrollRef = node;
170
- requestAnimationFrame(() => {
171
- updateScrollShadows();
172
- });
177
+ scrollResizeObserver === null || scrollResizeObserver === void 0 ? void 0 : scrollResizeObserver.observe(node);
173
178
  };
174
179
  </script>
175
180
 
@@ -179,10 +184,6 @@ const onScrollMounted = (node) => {
179
184
  {#snippet categoriesSwitcher(data: { maxItemsWidth: Number; onMounted: (data: { height: Number }) => void })}
180
185
  <div class="media-center" use:mounted={data.onMounted}>
181
186
  <div class="media-center__row" style={`max-width: ${data.maxItemsWidth}px;`}>
182
- <button type="button" class="media-center__overview-button" onclick={toggleOverview}>
183
- <Icon src={IconTextColumnThree} />
184
- </button>
185
-
186
187
  <div
187
188
  class="media-center__scroll"
188
189
  class:media-center__scroll--has-left={scrollHasLeft}
@@ -190,17 +191,40 @@ const onScrollMounted = (node) => {
190
191
  class:media-center__scroll--has-both={scrollHasRight && scrollHasLeft}
191
192
  use:onScrollMounted
192
193
  onscroll={updateScrollShadows}>
193
- <div class="media-center__items">
194
+ <button type="button" class="media-center__overview-button" onclick={toggleOverview}>
195
+ <Icon src={IconTextColumnThree} />
196
+ </button>
197
+ {#each categories as category (category.id)}
198
+ <button
199
+ type="button"
200
+ class="media-center__category-button"
201
+ class:media-center__category-button--active={selectedCategoryId === category.id}
202
+ title={category.name}
203
+ onclick={() => selectCategory(category.id)}>{category.name}</button>
204
+ {/each}
205
+ </div>
206
+ </div>
207
+ <div class="media-center__overview-dropdown">
208
+ <Dropdown>
209
+ {#snippet trigger()}
210
+ <div class="media-center__overview-dropdown-trigger">
211
+ <Icon src={IconLineHorizontal3} color={IconColor.White}></Icon>
212
+ </div>
213
+ {/snippet}
214
+ <div class="media-center__overview-dropdown-content">
215
+ <button type="button" class="media-center__category-button media-center__category-button--dropdown" onclick={toggleOverview}>
216
+ {localization.overviewLabel}
217
+ </button>
194
218
  {#each categories as category (category.id)}
195
219
  <button
196
220
  type="button"
197
- class="media-center__category-button"
221
+ class="media-center__category-button media-center__category-button--dropdown"
198
222
  class:media-center__category-button--active={selectedCategoryId === category.id}
199
223
  title={category.name}
200
224
  onclick={() => selectCategory(category.id)}>{category.name}</button>
201
225
  {/each}
202
226
  </div>
203
- </div>
227
+ </Dropdown>
204
228
  </div>
205
229
  </div>
206
230
  {/snippet}
@@ -246,12 +270,6 @@ const onScrollMounted = (node) => {
246
270
  right: 0;
247
271
  z-index: 1;
248
272
  pointer-events: none;
249
- /* Set 'container-type: inline-size;' to reference container*/
250
- }
251
- @container (width < 576px) {
252
- .media-center {
253
- padding-left: 1.25rem;
254
- }
255
273
  }
256
274
  .media-center__row {
257
275
  pointer-events: auto;
@@ -259,18 +277,29 @@ const onScrollMounted = (node) => {
259
277
  width: 100%;
260
278
  display: flex;
261
279
  align-items: center;
280
+ justify-content: center;
262
281
  gap: 0.75rem;
282
+ /* Set 'container-type: inline-size;' to reference container*/
283
+ }
284
+ @container (width < 576px) {
285
+ .media-center__row {
286
+ display: none;
287
+ }
263
288
  }
264
289
  .media-center__scroll {
265
290
  pointer-events: auto;
266
291
  position: relative;
267
292
  flex: 1 1 auto;
293
+ max-width: max-content;
268
294
  min-width: 0;
269
295
  overflow-x: auto;
270
296
  overflow-y: hidden;
271
297
  -webkit-overflow-scrolling: touch;
272
298
  scrollbar-width: none;
273
299
  display: flex;
300
+ align-items: center;
301
+ gap: 0.75rem;
302
+ flex-wrap: nowrap;
274
303
  mask-image: none;
275
304
  }
276
305
  .media-center__scroll::-webkit-scrollbar {
@@ -285,14 +314,6 @@ const onScrollMounted = (node) => {
285
314
  .media-center__scroll--has-both {
286
315
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgb(0, 0, 0) 32px, rgb(0, 0, 0) calc(100% - 32px), rgba(0, 0, 0, 0) 100%);
287
316
  }
288
- .media-center__items {
289
- display: inline-flex;
290
- align-items: center;
291
- gap: 0.75rem;
292
- flex-wrap: nowrap;
293
- pointer-events: none;
294
- padding-inline: 0.25rem;
295
- }
296
317
  .media-center__overview-button {
297
318
  pointer-events: auto;
298
319
  padding: 0.375rem 0.75rem;
@@ -329,8 +350,82 @@ const onScrollMounted = (node) => {
329
350
  background-color: rgba(255, 255, 255, 0.9);
330
351
  color: #000000;
331
352
  }
353
+ .media-center__category-button--dropdown {
354
+ width: max-content;
355
+ }
356
+ .media-center__overview-dropdown {
357
+ display: none;
358
+ pointer-events: auto;
359
+ /* Set 'container-type: inline-size;' to reference container*/
360
+ }
361
+ @container (width < 576px) {
362
+ .media-center__overview-dropdown {
363
+ display: block;
364
+ position: absolute;
365
+ top: 0.9375rem;
366
+ left: 0.625rem;
367
+ z-index: 1;
368
+ }
369
+ }
370
+ .media-center__overview-dropdown-trigger {
371
+ width: 3rem;
372
+ min-width: 3rem;
373
+ max-width: 3rem;
374
+ height: 3rem;
375
+ min-height: 3rem;
376
+ max-height: 3rem;
377
+ display: flex;
378
+ justify-content: center;
379
+ align-items: center;
380
+ background-color: rgba(0, 0, 0, 0.6);
381
+ border: 1px solid #1c1c1c;
382
+ border-radius: 50%;
383
+ text-align: center;
384
+ --icon--color: #ffffff;
385
+ --icon--size: 1.75rem;
386
+ }
387
+ .media-center__overview-dropdown-trigger:hover {
388
+ background-color: rgba(0, 0, 0, 0.9);
389
+ transition: background-color 0.5s;
390
+ }
391
+ .media-center__overview-dropdown-content {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 0.625rem;
395
+ }
332
396
 
333
397
  .media-center-overview {
334
398
  position: absolute;
335
399
  inset: 0;
400
+ container-type: inline-size;
401
+ overflow-y: auto;
402
+ scrollbar-color: transparent transparent;
403
+ scrollbar-width: thin;
404
+ }
405
+ .media-center-overview::-webkit-scrollbar {
406
+ width: 3px;
407
+ height: 3px;
408
+ background: var(--custom-scrollbar-background, transparent);
409
+ visibility: hidden;
410
+ }
411
+ .media-center-overview::-webkit-scrollbar-thumb {
412
+ background: transparent;
413
+ }
414
+ .media-center-overview:hover {
415
+ scrollbar-color: var(--custom-scrollbar-color, #7d7d7d) var(--custom-scrollbar-background, transparent);
416
+ scrollbar-width: thin;
417
+ }
418
+ .media-center-overview:hover::-webkit-scrollbar {
419
+ width: 3px;
420
+ height: 3px;
421
+ background: var(--custom-scrollbar-background, transparent);
422
+ visibility: hidden;
423
+ }
424
+ .media-center-overview:hover::-webkit-scrollbar-thumb {
425
+ background: var(--custom-scrollbar-color, #7d7d7d);
426
+ }
427
+ @media (max-width: 576px) {
428
+ .media-center-overview {
429
+ top: 5rem !important;
430
+ }
336
431
  }</style>
@@ -2,10 +2,12 @@ import { type Locale } from '../../core/locale';
2
2
  import type { IProductCardLocalization } from '../../products/product-card/product-card-localization';
3
3
  export interface IMediaCenterLocalization {
4
4
  shortVideosSectionTitle?: string;
5
+ overviewLabel?: string;
5
6
  productLocalization?: IProductCardLocalization | Locale;
6
7
  }
7
8
  export declare class MediaCenterLocalization {
8
9
  shortVideosSectionTitle: string;
10
+ overviewLabel: string;
9
11
  productLocalization: IProductCardLocalization | Locale;
10
12
  constructor(init: IMediaCenterLocalization | Locale);
11
13
  }
@@ -1,9 +1,11 @@
1
1
  import { isLocale } from '../../core/locale';
2
2
  export class MediaCenterLocalization {
3
3
  shortVideosSectionTitle;
4
+ overviewLabel;
4
5
  productLocalization;
5
6
  constructor(init) {
6
7
  this.shortVideosSectionTitle = isLocale(init) ? loc.shortVideosSectionTitle[init] : init.shortVideosSectionTitle || loc.shortVideosSectionTitle.en;
8
+ this.overviewLabel = isLocale(init) ? loc.overviewLabel[init] : init.overviewLabel || loc.overviewLabel.en;
7
9
  this.productLocalization = isLocale(init) ? init : init.productLocalization || 'en';
8
10
  }
9
11
  }
@@ -11,5 +13,9 @@ const loc = {
11
13
  shortVideosSectionTitle: {
12
14
  en: 'Popular Short Videos',
13
15
  no: 'Populære korte videoer'
16
+ },
17
+ overviewLabel: {
18
+ en: 'Overview',
19
+ no: 'Oversikt'
14
20
  }
15
21
  };
@@ -81,6 +81,12 @@ const shortVideoSectionItems = $derived.by(() => {
81
81
  display: flex;
82
82
  justify-content: center;
83
83
  padding: 1.25rem 1.875rem;
84
+ /* Set 'container-type: inline-size;' to reference container*/
85
+ }
86
+ @container (width < 576px) {
87
+ .media-center-overview {
88
+ padding: 0.625rem 0.9375rem;
89
+ }
84
90
  }
85
91
  .media-center-overview__content {
86
92
  width: 100%;
@@ -121,20 +127,23 @@ const shortVideoSectionItems = $derived.by(() => {
121
127
  display: grid;
122
128
  gap: 2rem 1.25rem;
123
129
  grid-template-columns: repeat(5, minmax(0, 1fr));
130
+ /* Set 'container-type: inline-size;' to reference container*/
131
+ /* Set 'container-type: inline-size;' to reference container*/
132
+ /* Set 'container-type: inline-size;' to reference container*/
124
133
  }
125
- @media (max-width: 768px) {
134
+ @container (width < 992px) {
126
135
  .media-center-overview__section-content {
127
136
  grid-template-columns: repeat(4, minmax(0, 1fr));
128
137
  }
129
138
  }
130
- @media (max-width: 576px) {
139
+ @container (width < 768px) {
131
140
  .media-center-overview__section-content {
132
141
  grid-template-columns: repeat(3, minmax(0, 1fr));
133
142
  }
134
143
  }
135
- @media (max-width: 480px) {
144
+ @container (width < 576px) {
136
145
  .media-center-overview__section-content {
137
- grid-template-columns: 2fr;
146
+ grid-template-columns: repeat(2, minmax(0, 1fr));
138
147
  }
139
148
  }
140
149
  .media-center-overview__card-wrapper {
@@ -13,7 +13,7 @@ const changeShowAttachments = () => {
13
13
  </script>
14
14
 
15
15
  {#if uiManager.viewInitialized && !uiManager.showShortVideoOverlay}
16
- <div class="short-videos-player-controls">
16
+ <div class="short-videos-player-controls" class:short-videos-player-controls--with-logo={!!playerLogo}>
17
17
  <div class="short-videos-player-controls__left">
18
18
  {#if shortVideo}
19
19
  <div class="short-videos-player-controls__short-video-hub">
@@ -86,6 +86,9 @@ const changeShowAttachments = () => {
86
86
  padding: var(--short-videos-player--controls--padding);
87
87
  container-type: inline-size;
88
88
  }
89
+ .short-videos-player-controls--with-logo {
90
+ padding-top: 0;
91
+ }
89
92
  .short-videos-player-controls__left {
90
93
  display: flex;
91
94
  flex-direction: column;
@@ -145,6 +148,7 @@ const changeShowAttachments = () => {
145
148
  height: var(--short-videos-player--media-center-header--height);
146
149
  min-height: var(--short-videos-player--media-center-header--height);
147
150
  max-height: var(--short-videos-player--media-center-header--height);
151
+ min-height: 4.375rem;
148
152
  display: flex;
149
153
  justify-content: center;
150
154
  align-items: center;
@@ -0,0 +1,187 @@
1
+ <script lang="ts">import { runningInBrowser } from '../../core/browser';
2
+ import { Icon } from '../icon';
3
+ import { isIgnored } from './dropdown-ignore';
4
+ import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
5
+ import { createPopper } from '@popperjs/core';
6
+ import { onDestroy } from 'svelte';
7
+ let { position = 'bottom-start', disabled = false, keepDropdownOpen = false, fixedPosition = false, offset = 8, boundaryMargin = 8, on, children, trigger, isOpenRequested } = $props();
8
+ $effect(() => {
9
+ var _a;
10
+ (_a = on === null || on === void 0 ? void 0 : on.mounted) === null || _a === void 0 ? void 0 : _a.call(on, {
11
+ toggleOpen: (value) => {
12
+ if (value === undefined) {
13
+ if (opened) {
14
+ close();
15
+ }
16
+ else {
17
+ open();
18
+ }
19
+ }
20
+ else {
21
+ if (!value) {
22
+ close();
23
+ }
24
+ else {
25
+ open();
26
+ }
27
+ }
28
+ }
29
+ });
30
+ });
31
+ const id = Math.random();
32
+ let opened = $state(false);
33
+ $effect(() => {
34
+ var _a, _b;
35
+ if (opened) {
36
+ (_a = on === null || on === void 0 ? void 0 : on.opened) === null || _a === void 0 ? void 0 : _a.call(on);
37
+ }
38
+ else {
39
+ (_b = on === null || on === void 0 ? void 0 : on.closed) === null || _b === void 0 ? void 0 : _b.call(on);
40
+ }
41
+ });
42
+ $effect(() => {
43
+ popper === null || popper === void 0 ? void 0 : popper.setOptions({ placement: position });
44
+ });
45
+ $effect(() => {
46
+ if (isOpenRequested) {
47
+ open();
48
+ }
49
+ });
50
+ let triggerRef = $state(null);
51
+ let dropdownRef;
52
+ let popper;
53
+ onDestroy(() => {
54
+ removeWindowClickListener();
55
+ });
56
+ const open = () => {
57
+ opened = true;
58
+ window.addEventListener('click', close);
59
+ };
60
+ const close = () => {
61
+ opened = false;
62
+ removeWindowClickListener();
63
+ };
64
+ const removeWindowClickListener = () => {
65
+ if (runningInBrowser()) {
66
+ window.removeEventListener('click', close);
67
+ }
68
+ };
69
+ const handleClick = (event) => {
70
+ event.stopPropagation();
71
+ if (disabled) {
72
+ return;
73
+ }
74
+ if (!opened) {
75
+ open();
76
+ return;
77
+ }
78
+ const checkCanClose = (node) => {
79
+ if (keepDropdownOpen) {
80
+ return false;
81
+ }
82
+ while (node && node !== dropdownRef) {
83
+ if (isIgnored(node) || node.classList.contains('flatpickr-calendar')) {
84
+ return false;
85
+ }
86
+ node = node.parentElement;
87
+ }
88
+ return true;
89
+ };
90
+ if (checkCanClose(event.target)) {
91
+ close();
92
+ }
93
+ };
94
+ const initPopper = (node, _triggerEl) => {
95
+ popper = createPopper(_triggerEl, node, {
96
+ placement: position,
97
+ strategy: fixedPosition ? 'fixed' : 'absolute',
98
+ modifiers: [
99
+ {
100
+ name: 'offset',
101
+ options: {
102
+ offset: () => {
103
+ return [0, offset];
104
+ }
105
+ }
106
+ },
107
+ { name: 'eventListeners', enabled: opened },
108
+ { name: 'flip' },
109
+ {
110
+ name: 'preventOverflow',
111
+ options: {
112
+ boundary: document.body,
113
+ padding: boundaryMargin
114
+ }
115
+ }
116
+ ]
117
+ });
118
+ return {
119
+ update(_triggerEl) {
120
+ popper.state.elements.reference = _triggerEl;
121
+ popper.update();
122
+ },
123
+ destroy() {
124
+ popper.destroy();
125
+ }
126
+ };
127
+ };
128
+ </script>
129
+
130
+ <div class="dropdown" class:dropdown--disabled={disabled} onclick={handleClick} onkeydown={() => ({})} bind:this={dropdownRef} role="none">
131
+ <button type="button" class="dropdown__trigger" bind:this={triggerRef}>
132
+ {#if trigger}
133
+ {@render trigger()}
134
+ {:else}
135
+ <Icon src={IconChevronDown} />
136
+ {/if}
137
+ </button>
138
+ {#if opened}
139
+ <div use:initPopper={triggerRef} class="dropdown__content" role="tooltip" tabindex="-1">
140
+ {@render children()}
141
+ </div>
142
+ {/if}
143
+ </div>
144
+
145
+ <style>@keyframes fadeIn {
146
+ 0% {
147
+ opacity: 1;
148
+ }
149
+ 50% {
150
+ opacity: 0.4;
151
+ }
152
+ 100% {
153
+ opacity: 1;
154
+ }
155
+ }
156
+ .dropdown {
157
+ --_dropdown--width: var(--dropdown--width, auto);
158
+ --_dropdown--content--background-color: var(--dropdown--content--background-color, transparent);
159
+ --_dropdown--content--box-shadow: var(--dropdown--content--box-shadow, none);
160
+ height: 100%;
161
+ width: var(--_dropdown--width);
162
+ -webkit-user-drag: none;
163
+ user-select: none;
164
+ }
165
+ .dropdown :global([contenteditable]) {
166
+ user-select: text;
167
+ }
168
+ .dropdown__trigger {
169
+ height: 100%;
170
+ width: 100%;
171
+ display: flex;
172
+ align-items: center;
173
+ line-height: 1;
174
+ }
175
+ .dropdown--disabled {
176
+ opacity: 0.5;
177
+ pointer-events: none;
178
+ }
179
+ .dropdown__content {
180
+ box-shadow: var(--_dropdown--content--box-shadow);
181
+ background: var(--_dropdown--content--background-color);
182
+ width: max-content;
183
+ z-index: 999;
184
+ }
185
+ .dropdown :global([data-popper-escaped]) {
186
+ visibility: hidden !important;
187
+ }</style>
@@ -0,0 +1,23 @@
1
+ import type { DropdownPosition } from './index';
2
+ import { type Snippet } from 'svelte';
3
+ type Props = {
4
+ position?: DropdownPosition;
5
+ disabled?: boolean;
6
+ keepDropdownOpen?: boolean;
7
+ fixedPosition?: boolean;
8
+ offset?: number;
9
+ boundaryMargin?: number;
10
+ on?: {
11
+ opened?: () => void;
12
+ closed?: () => void;
13
+ mounted?: (callbacks: {
14
+ toggleOpen: (value?: boolean) => void;
15
+ }) => void;
16
+ };
17
+ children: Snippet;
18
+ trigger?: Snippet;
19
+ isOpenRequested?: boolean;
20
+ };
21
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
22
+ type Cmp = ReturnType<typeof Cmp>;
23
+ export default Cmp;
@@ -0,0 +1,6 @@
1
+ /** @type {import('svelte/action').Action} */
2
+ export declare const dropdownIgnore: (node: HTMLElement, value?: boolean) => {
3
+ destroy(): void;
4
+ };
5
+ export declare const ignoreAttribute = "dropdownIgnore";
6
+ export declare const isIgnored: (node: HTMLElement) => boolean;
@@ -0,0 +1,11 @@
1
+ /** @type {import('svelte/action').Action} */
2
+ export const dropdownIgnore = (node, value = true) => {
3
+ node.dataset[ignoreAttribute] = value.toString();
4
+ return {
5
+ destroy() {
6
+ // the node has been removed from the DOM
7
+ }
8
+ };
9
+ };
10
+ export const ignoreAttribute = 'dropdownIgnore';
11
+ export const isIgnored = (node) => node.dataset[ignoreAttribute] === 'true';
@@ -0,0 +1,3 @@
1
+ export { default as Dropdown } from './cmp.dropdown.svelte';
2
+ export type DropdownPosition = import('@popperjs/core').Placement;
3
+ export { dropdownIgnore } from './dropdown-ignore';
@@ -0,0 +1,2 @@
1
+ export { default as Dropdown } from './cmp.dropdown.svelte';
2
+ export { dropdownIgnore } from './dropdown-ignore';
@@ -1,6 +1,5 @@
1
- <script lang="ts">import { isScrollingPrevented } from './prevent-slider-scroll';
1
+ <script lang="ts">import { createWheelPeakDetector } from './wheel-peak-detector';
2
2
  import { onDestroy, onMount, untrack } from 'svelte';
3
- const MOUSE_DETECTION_THRESHOLD_MS = 100;
4
3
  let { buffer, on, children } = $props();
5
4
  let slidesRef;
6
5
  let sliderHeight = $state(0);
@@ -90,89 +89,6 @@ onMount(() => {
90
89
  }
91
90
  reset();
92
91
  });
93
- let waveDetector = {
94
- events: [],
95
- direction: 0,
96
- peakReached: false,
97
- lastPeak: 0,
98
- waveStartTime: 0
99
- };
100
- let isAnimatingWheel = false;
101
- const triggerAnimation = (direction) => {
102
- isAnimatingWheel = true;
103
- if (direction > 0 && buffer.canLoadNext) {
104
- buffer.loadNext();
105
- }
106
- else if (direction < 0 && buffer.canLoadPrevious) {
107
- buffer.loadPrevious();
108
- }
109
- setTimeout(() => {
110
- isAnimatingWheel = false;
111
- }, buffer.animationDuration + 100);
112
- };
113
- slidesRef.addEventListener('wheel', (e) => {
114
- e.preventDefault();
115
- const checkCanHandleWheel = (node) => {
116
- while (node && node !== slidesRef) {
117
- if (isScrollingPrevented(node)) {
118
- return false;
119
- }
120
- node = node.parentElement;
121
- }
122
- return true;
123
- };
124
- if (!checkCanHandleWheel(e.target)) {
125
- return;
126
- }
127
- const now = Date.now();
128
- const absDelta = Math.abs(e.deltaY);
129
- const direction = Math.sign(e.deltaY);
130
- // Mouse - large stable values, trigger immediately
131
- if (absDelta >= 10 && !isAnimatingWheel) {
132
- const lastEvent = waveDetector.events[waveDetector.events.length - 1];
133
- const timeSinceLastEvent = lastEvent ? now - lastEvent.time : 1000;
134
- // If enough time has passed since the last event - it's a mouse
135
- if (timeSinceLastEvent > MOUSE_DETECTION_THRESHOLD_MS) {
136
- triggerAnimation(direction);
137
- waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
138
- return;
139
- }
140
- }
141
- // Touchpad - small, variable values, need to analyze the wave
142
- // New wave if: direction changed or a lot of time has passed
143
- if (direction !== waveDetector.direction || (waveDetector.waveStartTime && now - waveDetector.waveStartTime > 1500)) {
144
- // Finalize the previous wave if it existed
145
- if (waveDetector.peakReached && !isAnimatingWheel) {
146
- triggerAnimation(waveDetector.direction);
147
- }
148
- // Start a new wave
149
- waveDetector = {
150
- events: [{ delta: absDelta, time: now }],
151
- direction: direction,
152
- peakReached: false,
153
- lastPeak: absDelta,
154
- waveStartTime: now
155
- };
156
- return;
157
- }
158
- // Continue the current wave
159
- waveDetector.events.push({ delta: absDelta, time: now });
160
- // Determine the phase of the wave
161
- if (absDelta > waveDetector.lastPeak) {
162
- // The wave is growing
163
- waveDetector.lastPeak = absDelta;
164
- waveDetector.peakReached = absDelta >= 5; // The minimum peak to consider a valid wave
165
- }
166
- else if (absDelta < waveDetector.lastPeak * 0.5) {
167
- // The wave has dropped significantly - consider the gesture complete
168
- if (waveDetector.peakReached && !isAnimatingWheel) {
169
- triggerAnimation(waveDetector.direction);
170
- waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
171
- }
172
- }
173
- // Cleanup old events
174
- waveDetector.events = waveDetector.events.filter((evt) => now - evt.time < 2000);
175
- });
176
92
  slidesRef.addEventListener('transitionend', (e) => {
177
93
  if (e.target !== slidesRef) {
178
94
  return;
@@ -199,10 +115,24 @@ const styles = $derived.by(() => {
199
115
  ];
200
116
  return values.join(';');
201
117
  });
118
+ const peakDetectorCallbacks = {
119
+ canLoadNext: () => buffer.canLoadNext,
120
+ canLoadPrevious: () => buffer.canLoadPrevious,
121
+ onTrigger: (direction) => {
122
+ // direction: 1 -> next, -1 -> previous
123
+ if (direction > 0) {
124
+ buffer.loadNext();
125
+ }
126
+ else if (direction < 0) {
127
+ buffer.loadPrevious();
128
+ }
129
+ },
130
+ getAnimationDurationMs: () => buffer.animationDuration
131
+ };
202
132
  </script>
203
133
 
204
134
  <div class="player-slider">
205
- <div class="player-slider__slides" bind:this={slidesRef} style={styles}>
135
+ <div class="player-slider__slides" bind:this={slidesRef} use:createWheelPeakDetector={{ cbs: peakDetectorCallbacks }} style={styles}>
206
136
  {#each buffer.loaded as item, index (item)}
207
137
  <div class="player-slider__slide">
208
138
  {#if index >= activeIndex - 1 && index <= activeIndex + 1}
@@ -0,0 +1,22 @@
1
+ export type PeakDetectorConfig = {
2
+ mouseGapMs?: number;
3
+ mouseDeltaThreshold?: number;
4
+ interEventTimeout?: number;
5
+ minPeakToTrigger?: number;
6
+ waveMaxAgeMs?: number;
7
+ directionFlipEndsWave?: boolean;
8
+ animationCooldownMs?: number;
9
+ directionChangeMinAbsDelta?: number;
10
+ };
11
+ export type PeakDetectorCallbacks = {
12
+ canLoadNext: () => boolean;
13
+ canLoadPrevious: () => boolean;
14
+ onTrigger: (direction: number) => void;
15
+ getAnimationDurationMs: () => number;
16
+ };
17
+ export declare const createWheelPeakDetector: (target: HTMLElement, params: {
18
+ cbs: PeakDetectorCallbacks;
19
+ cfg?: PeakDetectorConfig;
20
+ }) => {
21
+ destroy(): void;
22
+ };
@@ -0,0 +1,157 @@
1
+ import { isScrollingPrevented } from './prevent-slider-scroll';
2
+ export const createWheelPeakDetector = (target, params) => {
3
+ const { cbs, cfg } = params;
4
+ // Defaults tuned for smooth touchpads; adjust in caller if needed.
5
+ const config = {
6
+ mouseGapMs: cfg?.mouseGapMs ?? 120,
7
+ mouseDeltaThreshold: cfg?.mouseDeltaThreshold ?? 12,
8
+ interEventTimeout: cfg?.interEventTimeout ?? 180,
9
+ minPeakToTrigger: cfg?.minPeakToTrigger ?? 6,
10
+ waveMaxAgeMs: cfg?.waveMaxAgeMs ?? 1800,
11
+ directionFlipEndsWave: cfg?.directionFlipEndsWave ?? true,
12
+ animationCooldownMs: cfg?.animationCooldownMs ?? 100, // extra after CSS transition
13
+ directionChangeMinAbsDelta: cfg?.directionChangeMinAbsDelta ?? 2 // ignore tiny opposite spikes
14
+ };
15
+ let wave = null;
16
+ let lastWheelTime = 0;
17
+ let lastDelta = 0;
18
+ let isAnimating = false;
19
+ let cooldownTimer = null;
20
+ const clearWave = () => {
21
+ wave = null;
22
+ };
23
+ const setAnimatingWithCooldown = () => {
24
+ isAnimating = true;
25
+ const total = cbs.getAnimationDurationMs() + config.animationCooldownMs;
26
+ if (cooldownTimer) {
27
+ clearTimeout(cooldownTimer);
28
+ }
29
+ cooldownTimer = window.setTimeout(() => {
30
+ isAnimating = false;
31
+ cooldownTimer = null;
32
+ }, total);
33
+ };
34
+ const trigger = (direction) => {
35
+ if (direction > 0 && !cbs.canLoadNext()) {
36
+ return;
37
+ }
38
+ if (direction < 0 && !cbs.canLoadPrevious()) {
39
+ return;
40
+ }
41
+ setAnimatingWithCooldown();
42
+ cbs.onTrigger(direction);
43
+ };
44
+ const canHandle = (node) => {
45
+ while (node && node !== target) {
46
+ if (isScrollingPrevented(node)) {
47
+ return false;
48
+ }
49
+ node = node.parentElement;
50
+ }
51
+ return true;
52
+ };
53
+ const finalizeWaveWithTrigger = () => {
54
+ if (wave && !isAnimating) {
55
+ if (wave.maxAbsDelta >= config.minPeakToTrigger) {
56
+ trigger(wave.direction);
57
+ }
58
+ }
59
+ clearWave();
60
+ };
61
+ const finalizeWaveSilently = () => {
62
+ // Finish the wave without triggering (used on direction flip to avoid ghost slide)
63
+ clearWave();
64
+ };
65
+ const onWheel = (e) => {
66
+ e.preventDefault();
67
+ if (!canHandle(e.target)) {
68
+ return;
69
+ }
70
+ const now = Date.now();
71
+ const dy = e.deltaY;
72
+ const absDelta = Math.abs(dy);
73
+ console.warn(absDelta);
74
+ const dir = Math.sign(dy);
75
+ // Mouse branch: big, sparse deltas
76
+ const timeSinceLast = now - lastWheelTime;
77
+ if (absDelta >= config.mouseDeltaThreshold && timeSinceLast > config.mouseGapMs && !isAnimating) {
78
+ trigger(dir);
79
+ clearWave();
80
+ lastWheelTime = now;
81
+ lastDelta = dy;
82
+ return;
83
+ }
84
+ // Determine direction change with hysteresis to avoid micro flips
85
+ const directionChanged = !!wave && dir !== 0 && dir !== wave.direction && absDelta >= config.directionChangeMinAbsDelta;
86
+ const inactiveTooLong = wave && now - wave?.lastEventTime > config.interEventTimeout;
87
+ const waveTooOld = wave && now - wave?.startTime > config.waveMaxAgeMs;
88
+ // If direction flip should end the wave, do it silently (no trigger of the old direction)
89
+ if (wave && config.directionFlipEndsWave && directionChanged) {
90
+ finalizeWaveSilently();
91
+ // Start a new wave immediately in the new direction
92
+ wave = {
93
+ startTime: now,
94
+ lastEventTime: now,
95
+ direction: dir,
96
+ maxAbsDelta: absDelta,
97
+ sumAbsDelta: absDelta,
98
+ events: 1
99
+ };
100
+ lastWheelTime = now;
101
+ lastDelta = dy;
102
+ return;
103
+ }
104
+ // Finalize by inactivity or age (these DO trigger)
105
+ if (inactiveTooLong || waveTooOld) {
106
+ finalizeWaveWithTrigger();
107
+ }
108
+ // Start condition for new wave:
109
+ // - no wave
110
+ // - current amplitude grows vs previous
111
+ // - long gap since last wheel
112
+ const growing = absDelta > Math.abs(lastDelta);
113
+ const longGap = timeSinceLast > config.interEventTimeout;
114
+ if (!wave || growing || longGap) {
115
+ // Opportunistic finalize only if not animating and not caused by a flip
116
+ // (flip branch handled earlier and is silent)
117
+ if (wave && !isAnimating) {
118
+ if (wave.maxAbsDelta >= config.minPeakToTrigger) {
119
+ trigger(wave.direction);
120
+ }
121
+ }
122
+ wave = {
123
+ startTime: now,
124
+ lastEventTime: now,
125
+ direction: dir || (wave?.direction ?? 0),
126
+ maxAbsDelta: absDelta,
127
+ sumAbsDelta: absDelta,
128
+ events: 1
129
+ };
130
+ lastWheelTime = now;
131
+ lastDelta = dy;
132
+ return;
133
+ }
134
+ // Continue current wave
135
+ if (wave) {
136
+ wave.lastEventTime = now;
137
+ wave.events += 1;
138
+ wave.sumAbsDelta += absDelta;
139
+ if (absDelta > wave.maxAbsDelta) {
140
+ wave.maxAbsDelta = absDelta; // peak update
141
+ }
142
+ }
143
+ lastWheelTime = now;
144
+ lastDelta = dy;
145
+ };
146
+ target.addEventListener('wheel', onWheel, { passive: false });
147
+ return {
148
+ destroy() {
149
+ target.removeEventListener('wheel', onWheel);
150
+ if (cooldownTimer) {
151
+ clearTimeout(cooldownTimer);
152
+ cooldownTimer = null;
153
+ }
154
+ wave = null;
155
+ }
156
+ };
157
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/embeddable",
3
- "version": "6.0.0",
3
+ "version": "6.0.2",
4
4
  "author": "StreamsCloud",
5
5
  "repository": "https://github.com/StreamsCloud/streamscloud-frontend-packages.git",
6
6
  "type": "module",
@@ -152,5 +152,8 @@
152
152
  "typescript-eslint": "^8.32.1",
153
153
  "vite": "^6.3.5",
154
154
  "vite-tsconfig-paths": "^5.1.4"
155
+ },
156
+ "dependencies": {
157
+ "@popperjs/core": "^2.11.8"
155
158
  }
156
159
  }