@streamscloud/embeddable 4.0.0 → 5.0.0

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 (58) hide show
  1. package/dist/ads/ad-card/cmp.ad-card.svelte +34 -7
  2. package/dist/ads/ad-card/cmp.ad-card.svelte.d.ts +1 -1
  3. package/dist/ads/ad-card/mapper.js +1 -0
  4. package/dist/ads/ad-card/operations.generated.d.ts +1 -0
  5. package/dist/ads/ad-card/operations.generated.js +1 -0
  6. package/dist/ads/ad-card/operations.graphql +1 -0
  7. package/dist/ads/ad-card/types.d.ts +1 -0
  8. package/dist/products/product-card/cmp.product-card.svelte +3 -2
  9. package/dist/short-videos/short-video-viewer/cmp.short-video-controls.svelte +24 -13
  10. package/dist/short-videos/short-video-viewer/cmp.short-video-controls.svelte.d.ts +2 -2
  11. package/dist/short-videos/short-video-viewer/cmp.short-video-viewer.svelte +4 -3
  12. package/dist/short-videos/short-video-viewer/cmp.short-video-viewer.svelte.d.ts +3 -2
  13. package/dist/short-videos/short-video-viewer/index.d.ts +2 -2
  14. package/dist/short-videos/short-video-viewer/index.js +1 -1
  15. package/dist/short-videos/short-video-viewer/mapper.d.ts +1 -1
  16. package/dist/short-videos/short-video-viewer/mapper.js +38 -5
  17. package/dist/short-videos/short-video-viewer/operations.generated.d.ts +1 -0
  18. package/dist/short-videos/short-video-viewer/operations.generated.js +87 -98
  19. package/dist/short-videos/short-video-viewer/operations.graphql +40 -2
  20. package/dist/short-videos/short-video-viewer/types.d.ts +35 -6
  21. package/dist/short-videos/short-video-viewer/types.js +1 -1
  22. package/dist/short-videos/short-video-viewer/ui-manager.svelte.js +2 -2
  23. package/dist/short-videos/short-videos-player/cmp.short-videos-player.svelte +3 -1
  24. package/dist/short-videos/short-videos-player/cmp.short-videos-player.svelte.d.ts +4 -0
  25. package/dist/short-videos/short-videos-player/controls.svelte +37 -20
  26. package/dist/short-videos/short-videos-player/controls.svelte.d.ts +2 -1
  27. package/dist/short-videos/short-videos-player/index.js +6 -0
  28. package/dist/short-videos/short-videos-player/operations.generated.d.ts +1 -0
  29. package/dist/short-videos/short-videos-player/operations.generated.js +73 -84
  30. package/dist/short-videos/short-videos-player/short-videos-player-view.svelte +19 -6
  31. package/dist/short-videos/short-videos-player/short-videos-player-view.svelte.d.ts +4 -0
  32. package/dist/short-videos/short-videos-player/types.d.ts +9 -9
  33. package/dist/short-videos/short-videos-player/ui-manager.svelte.d.ts +1 -0
  34. package/dist/short-videos/short-videos-player/ui-manager.svelte.js +2 -1
  35. package/dist/streams/layout/models/mapper.js +2 -1
  36. package/dist/streams/layout/models/stream-layout-short-video-model.d.ts +1 -0
  37. package/dist/streams/stream-player/cmp.stream-player.svelte +6 -4
  38. package/dist/streams/stream-player/cmp.stream-player.svelte.d.ts +2 -0
  39. package/dist/streams/stream-player/controls.svelte +25 -5
  40. package/dist/streams/stream-player/controls.svelte.d.ts +2 -0
  41. package/dist/streams/stream-player/index.d.ts +2 -0
  42. package/dist/streams/stream-player/index.js +2 -1
  43. package/dist/streams/stream-player/mapper.d.ts +1 -1
  44. package/dist/streams/stream-player/mapper.js +1 -1
  45. package/dist/streams/stream-player/ui-manager.svelte.d.ts +1 -0
  46. package/dist/streams/stream-player/ui-manager.svelte.js +2 -1
  47. package/dist/ui/line-clamp/cmp.line-clamp.svelte +35 -13
  48. package/dist/ui/player/cmp.player-slider.svelte +74 -9
  49. package/dist/ui/progress/cmp.progress.svelte +4 -1
  50. package/dist/ui/seek-bar/cmp.seek-bar.svelte +112 -28
  51. package/dist/ui/seek-bar/cmp.seek-bar.svelte.d.ts +3 -0
  52. package/dist/ui/video/cmp.video.svelte +45 -37
  53. package/dist/ui/video/cmp.video.svelte.d.ts +2 -0
  54. package/dist/ui/video/index.d.ts +1 -0
  55. package/dist/ui/video/index.js +1 -0
  56. package/dist/ui/video/types.d.ts +4 -0
  57. package/dist/ui/video/types.js +5 -0
  58. package/package.json +5 -1
@@ -19,7 +19,7 @@ import { PlayerSlider } from '../../ui/player';
19
19
  import { SpotlightLayout } from '../../ui/spotlight-layout';
20
20
  import { SwipeIndicator } from '../../ui/swipe-indicator';
21
21
  import { default as Controls } from './controls.svelte';
22
- import { mapStreamPlayerModel } from './mapper';
22
+ import { mapToStreamPlayerModel } from './mapper';
23
23
  import { GetStreamDocument } from './operations.generated';
24
24
  import { default as Overview } from './stream-overview.svelte';
25
25
  import { StreamPlayerBuffer } from './stream-player-buffer.svelte';
@@ -27,7 +27,7 @@ import { StreamPlayerLocalization } from './stream-player-localization';
27
27
  import { StreamPlayerUiManager } from './ui-manager.svelte';
28
28
  import { AppEventsTracker } from '@streamscloud/streams-analytics-collector';
29
29
  import { onMount } from 'svelte';
30
- let { streamId, graphqlOrigin, localization: localizationInit = 'en', showStreamsCloudWatermark, initiator, on } = $props();
30
+ let { streamId, graphqlOrigin, localization: localizationInit = 'en', showStreamsCloudWatermark, shortVideoSocialInteractionsHandler, initiator, on } = $props();
31
31
  const localization = $derived(new StreamPlayerLocalization(localizationInit));
32
32
  let model = $state(null);
33
33
  let buffer = $state.raw(null);
@@ -72,7 +72,7 @@ onMount(() => __awaiter(void 0, void 0, void 0, function* () {
72
72
  image: ((_d = streamPayload.data.stream.cover) === null || _d === void 0 ? void 0 : _d.url) || null
73
73
  });
74
74
  // start tracking the stream
75
- model = mapStreamPlayerModel(streamPayload.data.stream);
75
+ model = mapToStreamPlayerModel(streamPayload.data.stream);
76
76
  buffer = new StreamPlayerBuffer({ graphql, streamId });
77
77
  AppEventsTracker.trackStreamView(streamPayload.data.stream.id);
78
78
  startActivityTracking();
@@ -211,6 +211,7 @@ const onProgress = (pageId, videoId, progress) => {
211
211
  progress: (progress) => onProgress(item.id, item.shortVideo.id, progress)
212
212
  }}
213
213
  autoplay="on-appearance"
214
+ socialInteractionsHandler={shortVideoSocialInteractionsHandler}
214
215
  localization={localization.shortVideoViewerLocalization}
215
216
  showAttachments={uiManager.showShortVideoOverlay}
216
217
  showControls={uiManager.showShortVideoOverlay} />
@@ -238,6 +239,7 @@ const onProgress = (pageId, videoId, progress) => {
238
239
  buffer={buffer}
239
240
  uiManager={uiManager}
240
241
  localization={localization}
242
+ shortVideoSocialInteractionsHandler={shortVideoSocialInteractionsHandler}
241
243
  on={{
242
244
  closePlayer: () => onPlayerClose(),
243
245
  productClick: (productId: String) => onProductCardClick(productId)
@@ -267,7 +269,7 @@ const onProgress = (pageId, videoId, progress) => {
267
269
  container-type: inline-size;
268
270
  display: flex;
269
271
  position: relative;
270
- background-color: rgba(0, 0, 0, 0.75);
272
+ background-color: #c1c1c1;
271
273
  background-image: var(--background-image);
272
274
  background-size: cover;
273
275
  background-blend-mode: multiply;
@@ -1,10 +1,12 @@
1
1
  import type { Locale } from '../../core/locale';
2
+ import { type ShortVideoSocialInteractionsHanlder } from '../../short-videos/short-video-viewer';
2
3
  import { type IStreamPlayerLocalization } from './stream-player-localization';
3
4
  type Props = {
4
5
  streamId: string;
5
6
  localization?: IStreamPlayerLocalization | Locale;
6
7
  graphqlOrigin?: string;
7
8
  showStreamsCloudWatermark?: boolean;
9
+ shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHanlder;
8
10
  initiator?: string;
9
11
  on?: {
10
12
  closePlayer?: () => void;
@@ -9,7 +9,7 @@ import { StreamPlayerLocalization } from './stream-player-localization';
9
9
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_28_regular.svg?raw';
10
10
  import IconChevronUp from '@fluentui/svg-icons/icons/chevron_up_28_regular.svg?raw';
11
11
  import IconDismiss from '@fluentui/svg-icons/icons/dismiss_28_regular.svg?raw';
12
- let { buffer, uiManager, localization, on } = $props();
12
+ let { buffer, uiManager, shortVideoSocialInteractionsHandler, localization, on } = $props();
13
13
  const shortVideo = $derived(((_a = buffer.current) === null || _a === void 0 ? void 0 : _a.type) === 'short-video' && ((_b = buffer.current) === null || _b === void 0 ? void 0 : _b.shortVideo) ? mapToShortVideoViewerModel(buffer.current.shortVideo) : null);
14
14
  const singleWebViewPage = $derived.by(() => {
15
15
  var _a;
@@ -46,7 +46,7 @@ const changeShowShortVideoAttachments = () => {
46
46
  };
47
47
  </script>
48
48
 
49
- {#if !uiManager.showShortVideoOverlay}
49
+ {#if uiManager.viewInitialized && !uiManager.showShortVideoOverlay}
50
50
  <div class="stream-player-controls">
51
51
  <div class="stream-player-controls__left">
52
52
  {#if shortVideo}
@@ -56,15 +56,22 @@ const changeShowShortVideoAttachments = () => {
56
56
  <ShortVideoViewerAttachmentsInline model={shortVideo} />
57
57
  </div>
58
58
  {/if}
59
- <ShortVideoControls model={shortVideo} on={{ attachmentsClicked: changeShowShortVideoAttachments }} />
59
+ <ShortVideoControls
60
+ model={shortVideo}
61
+ socialInteractionsHandler={shortVideoSocialInteractionsHandler}
62
+ on={{ attachmentsClicked: changeShowShortVideoAttachments }} />
60
63
  </div>
61
64
  {/if}
62
65
  <div class="stream-player-controls__navigation-buttons" class:stream-player-controls__navigation-buttons--single-web-view-page={singleWebViewPage}>
63
66
  <button type="button" class="navigation-button" disabled={!buffer.canLoadPrevious} onclick={buffer.loadPrevious}>
64
- <Icon src={IconChevronUp} />
67
+ <span class="navigation-button__icon">
68
+ <Icon src={IconChevronUp} />
69
+ </span>
65
70
  </button>
66
71
  <button type="button" class="navigation-button" disabled={!buffer.canLoadNext} onclick={buffer.loadNext}>
67
- <Icon src={IconChevronDown} />
72
+ <span class="navigation-button__icon">
73
+ <Icon src={IconChevronDown} />
74
+ </span>
68
75
  </button>
69
76
  </div>
70
77
  </div>
@@ -209,6 +216,10 @@ const changeShowShortVideoAttachments = () => {
209
216
  opacity: 0.5;
210
217
  cursor: default;
211
218
  }
219
+ .close-button:hover {
220
+ background-color: rgba(0, 0, 0, 0.9);
221
+ transition: background-color 0.5s;
222
+ }
212
223
  @container (width < 576px) {
213
224
  .close-button {
214
225
  left: unset;
@@ -217,6 +228,7 @@ const changeShowShortVideoAttachments = () => {
217
228
  }
218
229
 
219
230
  .navigation-button {
231
+ --_icon-scale: 1;
220
232
  width: var(--stream-player--button--size);
221
233
  min-width: var(--stream-player--button--size);
222
234
  max-width: var(--stream-player--button--size);
@@ -233,4 +245,12 @@ const changeShowShortVideoAttachments = () => {
233
245
  .navigation-button:disabled {
234
246
  opacity: 0.5;
235
247
  cursor: default;
248
+ }
249
+ .navigation-button:hover:not(:disabled) {
250
+ --_icon-scale: 1.2;
251
+ }
252
+ .navigation-button__icon {
253
+ display: block;
254
+ transform: scale(var(--_icon-scale));
255
+ transition: 0.3s;
236
256
  }</style>
@@ -1,9 +1,11 @@
1
+ import { type ShortVideoSocialInteractionsHanlder } from '../../short-videos/short-video-viewer';
1
2
  import type { StreamPlayerBuffer } from './stream-player-buffer.svelte';
2
3
  import { StreamPlayerLocalization } from './stream-player-localization';
3
4
  import type { StreamPlayerUiManager } from './ui-manager.svelte';
4
5
  type Props = {
5
6
  buffer: StreamPlayerBuffer;
6
7
  uiManager: StreamPlayerUiManager;
8
+ shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHanlder;
7
9
  localization: StreamPlayerLocalization;
8
10
  on: {
9
11
  closePlayer: () => void;
@@ -1,3 +1,4 @@
1
+ import type { ShortVideoSocialInteractionsHanlder } from '../../short-videos/short-video-viewer';
1
2
  import type { IStreamPlayerLocalization } from './stream-player-localization';
2
3
  export type { IStreamPlayerLocalization };
3
4
  /**
@@ -28,6 +29,7 @@ export declare const openStreamPlayer: (init: {
28
29
  graphqlOrigin?: string;
29
30
  localization?: IStreamPlayerLocalization | "en" | "no";
30
31
  showStreamsCloudWatermark?: boolean;
32
+ shortVideoSocialInteractionsHandler?: ShortVideoSocialInteractionsHanlder;
31
33
  initiator?: string;
32
34
  on?: {
33
35
  streamActivated?: (data: {
@@ -27,7 +27,7 @@ import { mount, unmount } from 'svelte';
27
27
  * ```
28
28
  */
29
29
  export const openStreamPlayer = (init) => {
30
- const { streamId, graphqlOrigin, localization, showStreamsCloudWatermark, initiator } = init;
30
+ const { streamId, graphqlOrigin, localization, showStreamsCloudWatermark, shortVideoSocialInteractionsHandler, initiator } = init;
31
31
  const shadowHost = new ModalShadowHost();
32
32
  const mounted = mount(StreamPlayer, {
33
33
  target: shadowHost.shadowRoot,
@@ -36,6 +36,7 @@ export const openStreamPlayer = (init) => {
36
36
  graphqlOrigin,
37
37
  localization: getLocale(localization),
38
38
  showStreamsCloudWatermark,
39
+ shortVideoSocialInteractionsHandler,
39
40
  initiator,
40
41
  on: {
41
42
  streamActivated: (data) => {
@@ -1,3 +1,3 @@
1
1
  import type { StreamPlayerPayloadFragment } from './operations.generated';
2
2
  import type { StreamPlayerModel } from './types';
3
- export declare const mapStreamPlayerModel: (payload: StreamPlayerPayloadFragment) => StreamPlayerModel;
3
+ export declare const mapToStreamPlayerModel: (payload: StreamPlayerPayloadFragment) => StreamPlayerModel;
@@ -1,4 +1,4 @@
1
- export const mapStreamPlayerModel = (payload) => {
1
+ export const mapToStreamPlayerModel = (payload) => {
2
2
  const headerDataProvider = payload.availableFrom ?? payload.ownerProfile;
3
3
  return {
4
4
  id: payload.id,
@@ -3,6 +3,7 @@ export declare class StreamPlayerUiManager {
3
3
  showShortVideoAttachments: boolean;
4
4
  globalCssVariables: string;
5
5
  isMobileView: boolean;
6
+ viewInitialized: boolean;
6
7
  showShortVideoOverlay: boolean;
7
8
  private readonly buttonSize;
8
9
  private readonly iconSize;
@@ -13,7 +13,8 @@ export class StreamPlayerUiManager {
13
13
  return values.join(';');
14
14
  });
15
15
  isMobileView = $derived.by(() => this.viewTotalWidth <= 576);
16
- showShortVideoOverlay = $derived.by(() => (this.viewTotalWidth - this.mainViewColumnWidth) / 2 <= 85);
16
+ viewInitialized = $derived.by(() => !!this.viewTotalWidth && !!this.mainViewColumnWidth);
17
+ showShortVideoOverlay = $derived.by(() => this.viewInitialized && (this.viewTotalWidth - this.mainViewColumnWidth) / 2 <= 85);
17
18
  buttonSize = 48;
18
19
  iconSize = 28;
19
20
  controlsOffsetHorizontal = 28;
@@ -51,20 +51,25 @@ const toggleShowMore = () => {
51
51
  </script>
52
52
 
53
53
  <div class="line-clamp" bind:this={element}>
54
- <div class="line-clamp__wrapper" bind:this={clampWrapperRef}>
55
- {#if children}
56
- {@render children()}
57
- {:else if value}
58
- {@html value}
54
+ <div class="line-clamp__wrapper-container">
55
+ <div class="line-clamp__wrapper" bind:this={clampWrapperRef}>
56
+ {#if children}
57
+ {@render children()}
58
+ {:else if value}
59
+ {@html value}
60
+ {/if}
61
+ </div>
62
+
63
+ {#if enableShowMore && isTruncated && !showingAllText}
64
+ <button type="button" class="line-clamp__show-more-button line-clamp__show-more-button--inline" onclick={toggleShowMore}>
65
+ {localization.showMore}
66
+ </button>
59
67
  {/if}
60
68
  </div>
61
- {#if enableShowMore && (isTruncated || showingAllText)}
69
+
70
+ {#if enableShowMore && showingAllText}
62
71
  <button type="button" class="line-clamp__show-more-button" onclick={toggleShowMore}>
63
- {#if showingAllText}
64
- {localization.showLess}
65
- {:else}
66
- {localization.showMore}
67
- {/if}
72
+ {localization.showLess}
68
73
  </button>
69
74
  {/if}
70
75
  </div>
@@ -75,6 +80,9 @@ const toggleShowMore = () => {
75
80
  display: flex;
76
81
  flex-direction: column;
77
82
  }
83
+ .line-clamp__wrapper-container {
84
+ position: relative;
85
+ }
78
86
  .line-clamp__wrapper {
79
87
  display: -webkit-box;
80
88
  overflow: hidden;
@@ -83,7 +91,21 @@ const toggleShowMore = () => {
83
91
  -webkit-box-orient: vertical;
84
92
  }
85
93
  .line-clamp__show-more-button {
86
- margin-top: 0.5em;
87
94
  font-size: 0.9em;
88
- opacity: 0.8;
95
+ font-style: italic;
96
+ }
97
+ .line-clamp__show-more-button--inline {
98
+ position: absolute;
99
+ bottom: 0;
100
+ right: 0;
101
+ padding-left: 1em;
102
+ backdrop-filter: blur(2px);
103
+ }
104
+ .line-clamp__show-more-button--inline::after {
105
+ content: "...";
106
+ position: absolute;
107
+ left: 0;
108
+ }
109
+ .line-clamp__show-more-button:not(.line-clamp__show-more-button--inline) {
110
+ margin-top: 0.5em;
89
111
  }</style>
@@ -1,5 +1,4 @@
1
- <script lang="ts">import { Utils } from '../../core/utils';
2
- import { isScrollingPrevented } from './prevent-slider-scroll';
1
+ <script lang="ts">import { isScrollingPrevented } from './prevent-slider-scroll';
3
2
  import { onDestroy, onMount, untrack } from 'svelte';
4
3
  let { buffer, on, children } = $props();
5
4
  let slidesRef;
@@ -90,7 +89,28 @@ onMount(() => {
90
89
  }
91
90
  reset();
92
91
  });
93
- slidesRef.addEventListener('wheel', Utils.throttle((e) => {
92
+ let waveDetector = {
93
+ events: [],
94
+ direction: 0,
95
+ peakReached: false,
96
+ lastPeak: 0,
97
+ waveStartTime: 0
98
+ };
99
+ let isAnimatingWheel = false;
100
+ const triggerAnimation = (direction) => {
101
+ isAnimatingWheel = true;
102
+ if (direction > 0 && buffer.canLoadNext) {
103
+ buffer.loadNext();
104
+ }
105
+ else if (direction < 0 && buffer.canLoadPrevious) {
106
+ buffer.loadPrevious();
107
+ }
108
+ setTimeout(() => {
109
+ isAnimatingWheel = false;
110
+ }, buffer.animationDuration + 100);
111
+ };
112
+ slidesRef.addEventListener('wheel', (e) => {
113
+ e.preventDefault();
94
114
  const checkCanHandleWheel = (node) => {
95
115
  while (node && node !== slidesRef) {
96
116
  if (isScrollingPrevented(node)) {
@@ -103,13 +123,55 @@ onMount(() => {
103
123
  if (!checkCanHandleWheel(e.target)) {
104
124
  return;
105
125
  }
106
- if (e.deltaY > 0 && buffer.canLoadNext) {
107
- buffer.loadNext();
126
+ const now = Date.now();
127
+ const absDelta = Math.abs(e.deltaY);
128
+ const direction = Math.sign(e.deltaY);
129
+ // Mouse - large stable values, trigger immediately
130
+ if (absDelta >= 10 && !isAnimatingWheel) {
131
+ const lastEvent = waveDetector.events[waveDetector.events.length - 1];
132
+ const timeSinceLastEvent = lastEvent ? now - lastEvent.time : 1000;
133
+ // If enough time has passed since the last event - it's a mouse
134
+ if (timeSinceLastEvent > 100) {
135
+ triggerAnimation(direction);
136
+ waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
137
+ return;
138
+ }
108
139
  }
109
- if (e.deltaY < 0 && buffer.canLoadPrevious) {
110
- buffer.loadPrevious();
140
+ // Touchpad - small, variable values, need to analyze the wave
141
+ // New wave if: direction changed or a lot of time has passed
142
+ if (direction !== waveDetector.direction || (waveDetector.waveStartTime && now - waveDetector.waveStartTime > 1500)) {
143
+ // Finalize the previous wave if it existed
144
+ if (waveDetector.peakReached && !isAnimatingWheel) {
145
+ triggerAnimation(waveDetector.direction);
146
+ }
147
+ // Start a new wave
148
+ waveDetector = {
149
+ events: [{ delta: absDelta, time: now }],
150
+ direction: direction,
151
+ peakReached: false,
152
+ lastPeak: absDelta,
153
+ waveStartTime: now
154
+ };
155
+ return;
156
+ }
157
+ // Continue the current wave
158
+ waveDetector.events.push({ delta: absDelta, time: now });
159
+ // Determine the phase of the wave
160
+ if (absDelta > waveDetector.lastPeak) {
161
+ // The wave is growing
162
+ waveDetector.lastPeak = absDelta;
163
+ waveDetector.peakReached = absDelta >= 5; // The minimum peak to consider a valid wave
111
164
  }
112
- }, buffer.animationDuration + 250));
165
+ else if (absDelta < waveDetector.lastPeak * 0.5) {
166
+ // The wave has dropped significantly - consider the gesture complete
167
+ if (waveDetector.peakReached && !isAnimatingWheel) {
168
+ triggerAnimation(waveDetector.direction);
169
+ waveDetector = { events: [], direction: 0, peakReached: false, lastPeak: 0, waveStartTime: 0 };
170
+ }
171
+ }
172
+ // Cleanup old events
173
+ waveDetector.events = waveDetector.events.filter((evt) => now - evt.time < 2000);
174
+ });
113
175
  slidesRef.addEventListener('transitionend', (e) => {
114
176
  if (e.target !== slidesRef) {
115
177
  return;
@@ -142,7 +204,10 @@ const styles = $derived.by(() => {
142
204
  <div class="player-slider__slides" bind:this={slidesRef} style={styles}>
143
205
  {#each buffer.loaded as item, index (item)}
144
206
  <div class="player-slider__slide">
145
- {@render children({ item, active: index === activeIndex })}
207
+ {#if index >= activeIndex - 1 && index <= activeIndex + 1}
208
+ <!-- Only render the active slide and its immediate neighbors for performance -->
209
+ {@render children({ item, active: index === activeIndex })}
210
+ {/if}
146
211
  </div>
147
212
  {/each}
148
213
  </div>
@@ -22,10 +22,13 @@
22
22
  --_progress--height: var(--progress--height, 0.25em);
23
23
  --_progress--back-color: var(--progress--back-color, #b0b0b0);
24
24
  --_progress--front-color: var(--progress--front-color, #ffffff);
25
+ --_progress--box-shadow: var(--progress--box-shadow, 0 2px 3px rgba(0, 0, 0, 0.25) inset);
26
+ --_progress--border-radius: var(--progress--border-radius, 0);
25
27
  width: 100%;
26
28
  background: var(--_progress--back-color);
27
29
  height: var(--_progress--height);
28
- box-shadow: 0 2px 3px rgba(0, 0, 0, 0.25) inset;
30
+ box-shadow: var(--_progress--box-shadow);
31
+ border-radius: var(--_progress--border-radius);
29
32
  }
30
33
  .progress__value {
31
34
  background: var(--_progress--front-color);
@@ -1,30 +1,104 @@
1
- <script lang="ts">let { value, on } = $props();
1
+ <script lang="ts">let { value, listenParentClicks = false, on } = $props();
2
+ let seekBarRef;
2
3
  let progressRef;
3
- let valueRef;
4
+ let isDragging = $state(false);
5
+ const cssValue = $derived(`${100 * (value <= 1 ? value : 1)}%`);
4
6
  const handleSeek = (e) => {
7
+ if (!progressRef) {
8
+ return;
9
+ }
5
10
  e.stopPropagation();
6
11
  const { left, width } = progressRef.getBoundingClientRect();
7
12
  const x = e.clientX - left;
8
- const percent = x / width;
9
- value = percent;
10
- valueRef.style.transition = 'none';
11
- valueRef.style.width = `${100 * percent}%`;
12
- setTimeout(() => {
13
- valueRef.style.transition = '';
14
- }, 0);
13
+ const percent = Math.max(0, Math.min(1, x / width));
15
14
  on === null || on === void 0 ? void 0 : on.seek(percent);
16
15
  };
16
+ const handleParentClick = (e) => {
17
+ e.preventDefault();
18
+ e.stopPropagation();
19
+ if (!progressRef || !seekBarRef || isDragging) {
20
+ return;
21
+ }
22
+ const seekBarRect = progressRef.getBoundingClientRect();
23
+ if (e.clientX >= seekBarRect.left && e.clientX <= seekBarRect.right) {
24
+ const x = e.clientX - seekBarRect.left;
25
+ const percent = Math.max(0, Math.min(1, x / seekBarRect.width));
26
+ on === null || on === void 0 ? void 0 : on.seek(percent);
27
+ }
28
+ };
29
+ const onMouseMove = (e) => {
30
+ if (isDragging) {
31
+ handleSeek(e);
32
+ }
33
+ };
34
+ const onMouseUp = () => {
35
+ var _a;
36
+ isDragging = false;
37
+ window.removeEventListener('mousemove', onMouseMove);
38
+ window.removeEventListener('mouseup', onMouseUp);
39
+ (_a = on === null || on === void 0 ? void 0 : on.dragEnd) === null || _a === void 0 ? void 0 : _a.call(on);
40
+ };
41
+ const onMouseDown = (e) => {
42
+ var _a;
43
+ isDragging = true;
44
+ (_a = on === null || on === void 0 ? void 0 : on.dragStart) === null || _a === void 0 ? void 0 : _a.call(on);
45
+ handleSeek(e);
46
+ window.addEventListener('mousemove', onMouseMove);
47
+ window.addEventListener('mouseup', onMouseUp);
48
+ };
49
+ const handleScrubberKeyDown = (event) => {
50
+ const step = 0.05;
51
+ let newValue;
52
+ if (event.key === 'ArrowLeft') {
53
+ newValue = Math.max(0, value - step);
54
+ }
55
+ else if (event.key === 'ArrowRight') {
56
+ newValue = Math.min(1, value + step);
57
+ }
58
+ else if (event.key === 'Home') {
59
+ newValue = 0;
60
+ }
61
+ else if (event.key === 'End') {
62
+ newValue = 1;
63
+ }
64
+ if (newValue !== undefined) {
65
+ event.preventDefault();
66
+ on === null || on === void 0 ? void 0 : on.seek(newValue);
67
+ }
68
+ };
69
+ $effect(() => {
70
+ if (listenParentClicks && seekBarRef) {
71
+ const parent = seekBarRef.parentElement;
72
+ if (parent) {
73
+ parent.addEventListener('click', handleParentClick);
74
+ return () => {
75
+ parent.removeEventListener('click', handleParentClick);
76
+ };
77
+ }
78
+ }
79
+ });
80
+ $effect(() => {
81
+ return () => {
82
+ window.removeEventListener('mousemove', onMouseMove);
83
+ window.removeEventListener('mouseup', onMouseUp);
84
+ };
85
+ });
17
86
  </script>
18
87
 
19
- <div class="seek-bar" onclick={handleSeek} onkeydown={() => ({})} role="none">
88
+ <div class="seek-bar" onmousedown={onMouseDown} onkeydown={() => ({})} role="none" bind:this={seekBarRef}>
20
89
  <div class="seek-bar__container" bind:this={progressRef}>
21
- <span
22
- class="seek-bar__value"
23
- class:seek-bar__value--animate={value > 0.001 && value < 0.96}
24
- style="width: {`${100 * (value <= 1 ? value : 1)}%`}"
25
- bind:this={valueRef}>
26
- &nbsp;
27
- </span>
90
+ <span class="seek-bar__value" style="width: {cssValue}"> &nbsp; </span>
91
+ <div
92
+ class="seek-bar__scrubber"
93
+ style="left: {cssValue}"
94
+ role="slider"
95
+ tabindex="0"
96
+ aria-valuemin="0"
97
+ aria-valuemax="1"
98
+ aria-valuenow={value}
99
+ aria-label="Media position slider"
100
+ onkeydown={handleScrubberKeyDown}>
101
+ </div>
28
102
  </div>
29
103
  </div>
30
104
 
@@ -40,24 +114,34 @@ const handleSeek = (e) => {
40
114
  }
41
115
  }
42
116
  .seek-bar {
43
- --_seek-bar--height: var(--seek-bar--height, 0.25em);
44
- --_seek-bar--back-color: var(--seek-bar--back-color, #b0b0b0);
45
- --_seek-bar--front-color: var(--seek-bar--front-color, #ffffff);
46
117
  cursor: pointer;
118
+ position: relative;
119
+ --_seek-bar--container-color: var(--seek-bar--container-color, #b0b0b0);
120
+ --_seek-bar--value-color: var(--seek-bar--value-color, #fff);
121
+ --_seek-bar--scrubber-color: var(--seek-bar--scrubber-color, #fff);
47
122
  }
48
123
  .seek-bar__container {
49
124
  width: 100%;
50
- background: var(--_seek-bar--back-color);
51
- height: var(--_seek-bar--height);
52
- box-shadow: 0 2px 3px rgba(0, 0, 0, 0.25) inset;
53
- border-radius: calc(var(--_seek-bar--height) / 2);
54
- overflow: hidden;
125
+ background: var(--_seek-bar--container-color);
126
+ height: 0.3125rem;
127
+ position: relative;
55
128
  }
56
129
  .seek-bar__value {
57
- background: var(--_seek-bar--front-color);
130
+ background: var(--_seek-bar--value-color);
131
+ border-color: var(--_seek-bar--container-color);
132
+ border-width: 0.0625rem;
58
133
  display: inline-block;
59
134
  height: 100%;
60
135
  }
61
- .seek-bar__value--animate {
62
- transition: width 500ms;
136
+ .seek-bar__scrubber {
137
+ position: absolute;
138
+ top: 50%;
139
+ width: 0.75rem;
140
+ height: 0.75rem;
141
+ background: var(--_seek-bar--scrubber-color);
142
+ border-color: var(--_seek-bar--container-color);
143
+ border-width: 0.0625rem;
144
+ border-radius: 50%;
145
+ transform: translate(-50%, -50%);
146
+ z-index: 1;
63
147
  }</style>
@@ -3,8 +3,11 @@ type Props = {
3
3
  * 0-1
4
4
  * */
5
5
  value: number;
6
+ listenParentClicks?: boolean;
6
7
  on: {
7
8
  seek: (e: number) => void;
9
+ dragStart?: () => void;
10
+ dragEnd?: () => void;
8
11
  };
9
12
  };
10
13
  declare const Cmp: import("svelte").Component<Props, {}, "">;