@streamscloud/embeddable 4.0.0 → 5.1.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 +75 -9
  49. package/dist/ui/progress/cmp.progress.svelte +4 -1
  50. package/dist/ui/seek-bar/cmp.seek-bar.svelte +134 -28
  51. package/dist/ui/seek-bar/cmp.seek-bar.svelte.d.ts +3 -0
  52. package/dist/ui/video/cmp.video.svelte +40 -42
  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 ShortVideoSocialInteractionsHandler } 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?: ShortVideoSocialInteractionsHandler;
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 ShortVideoSocialInteractionsHandler } 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?: ShortVideoSocialInteractionsHandler;
7
9
  localization: StreamPlayerLocalization;
8
10
  on: {
9
11
  closePlayer: () => void;
@@ -1,3 +1,4 @@
1
+ import type { ShortVideoSocialInteractionsHandler } 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?: ShortVideoSocialInteractionsHandler;
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,6 +1,6 @@
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';
3
+ const MOUSE_DETECTION_THRESHOLD_MS = 100;
4
4
  let { buffer, on, children } = $props();
5
5
  let slidesRef;
6
6
  let sliderHeight = $state(0);
@@ -90,7 +90,28 @@ onMount(() => {
90
90
  }
91
91
  reset();
92
92
  });
93
- slidesRef.addEventListener('wheel', Utils.throttle((e) => {
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();
94
115
  const checkCanHandleWheel = (node) => {
95
116
  while (node && node !== slidesRef) {
96
117
  if (isScrollingPrevented(node)) {
@@ -103,13 +124,55 @@ onMount(() => {
103
124
  if (!checkCanHandleWheel(e.target)) {
104
125
  return;
105
126
  }
106
- if (e.deltaY > 0 && buffer.canLoadNext) {
107
- buffer.loadNext();
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
+ }
108
140
  }
109
- if (e.deltaY < 0 && buffer.canLoadPrevious) {
110
- buffer.loadPrevious();
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
111
165
  }
112
- }, buffer.animationDuration + 250));
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
+ });
113
176
  slidesRef.addEventListener('transitionend', (e) => {
114
177
  if (e.target !== slidesRef) {
115
178
  return;
@@ -142,7 +205,10 @@ const styles = $derived.by(() => {
142
205
  <div class="player-slider__slides" bind:this={slidesRef} style={styles}>
143
206
  {#each buffer.loaded as item, index (item)}
144
207
  <div class="player-slider__slide">
145
- {@render children({ item, active: index === activeIndex })}
208
+ {#if index >= activeIndex - 1 && index <= activeIndex + 1}
209
+ <!-- Only render the active slide and its immediate neighbors for performance -->
210
+ {@render children({ item, active: index === activeIndex })}
211
+ {/if}
146
212
  </div>
147
213
  {/each}
148
214
  </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,112 @@
1
- <script lang="ts">let { value, on } = $props();
1
+ <script lang="ts">let { value, listenParentClicks = false, on } = $props();
2
+ let seekBarRef;
3
+ let scrubberRef;
2
4
  let progressRef;
3
- let valueRef;
5
+ let isDragging = $state(false);
6
+ const cssValue = $derived(`${100 * (value <= 1 ? value : 1)}%`);
4
7
  const handleSeek = (e) => {
8
+ if (!progressRef) {
9
+ return;
10
+ }
5
11
  e.stopPropagation();
6
12
  const { left, width } = progressRef.getBoundingClientRect();
7
13
  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);
14
+ const percent = Math.max(0, Math.min(1, x / width));
15
15
  on === null || on === void 0 ? void 0 : on.seek(percent);
16
16
  };
17
+ const handleParentClick = (e) => {
18
+ e.preventDefault();
19
+ e.stopPropagation();
20
+ if (!progressRef || !seekBarRef || isDragging) {
21
+ return;
22
+ }
23
+ const seekBarRect = progressRef.getBoundingClientRect();
24
+ if (e.clientX >= seekBarRect.left && e.clientX <= seekBarRect.right) {
25
+ const x = e.clientX - seekBarRect.left;
26
+ const percent = Math.max(0, Math.min(1, x / seekBarRect.width));
27
+ on === null || on === void 0 ? void 0 : on.seek(percent);
28
+ }
29
+ };
30
+ const onMouseMove = (e) => {
31
+ if (isDragging) {
32
+ handleSeek(e);
33
+ }
34
+ };
35
+ const onMouseUp = () => {
36
+ var _a;
37
+ isDragging = false;
38
+ window.removeEventListener('mousemove', onMouseMove);
39
+ window.removeEventListener('mouseup', onMouseUp);
40
+ scrubberRef === null || scrubberRef === void 0 ? void 0 : scrubberRef.blur();
41
+ (_a = on === null || on === void 0 ? void 0 : on.dragEnd) === null || _a === void 0 ? void 0 : _a.call(on);
42
+ };
43
+ const onMouseDown = (e) => {
44
+ var _a;
45
+ isDragging = true;
46
+ (_a = on === null || on === void 0 ? void 0 : on.dragStart) === null || _a === void 0 ? void 0 : _a.call(on);
47
+ handleSeek(e);
48
+ window.addEventListener('mousemove', onMouseMove);
49
+ window.addEventListener('mouseup', onMouseUp);
50
+ scrubberRef === null || scrubberRef === void 0 ? void 0 : scrubberRef.blur();
51
+ };
52
+ const handleScrubberKeyDown = (event) => {
53
+ const step = 0.05;
54
+ let newValue;
55
+ if (event.key === 'ArrowLeft') {
56
+ newValue = Math.max(0, value - step);
57
+ }
58
+ else if (event.key === 'ArrowRight') {
59
+ newValue = Math.min(1, value + step);
60
+ }
61
+ else if (event.key === 'Home') {
62
+ newValue = 0;
63
+ }
64
+ else if (event.key === 'End') {
65
+ newValue = 1;
66
+ }
67
+ if (newValue !== undefined) {
68
+ event.preventDefault();
69
+ on === null || on === void 0 ? void 0 : on.seek(newValue);
70
+ }
71
+ };
72
+ $effect(() => {
73
+ let parent = null;
74
+ if (listenParentClicks && seekBarRef) {
75
+ parent = seekBarRef.parentElement;
76
+ if (parent) {
77
+ parent.addEventListener('click', handleParentClick);
78
+ }
79
+ }
80
+ return () => {
81
+ if (parent) {
82
+ parent.removeEventListener('click', handleParentClick);
83
+ }
84
+ };
85
+ });
86
+ $effect(() => {
87
+ return () => {
88
+ window.removeEventListener('mousemove', onMouseMove);
89
+ window.removeEventListener('mouseup', onMouseUp);
90
+ };
91
+ });
17
92
  </script>
18
93
 
19
- <div class="seek-bar" onclick={handleSeek} onkeydown={() => ({})} role="none">
94
+ <div class="seek-bar" onmousedown={onMouseDown} onkeydown={() => ({})} role="none" bind:this={seekBarRef}>
20
95
  <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>
96
+ <span class="seek-bar__value" style="width: {cssValue}"> &nbsp; </span>
97
+ <div
98
+ class="seek-bar__scrubber"
99
+ bind:this={scrubberRef}
100
+ class:is-dragging={isDragging}
101
+ style="left: {cssValue}"
102
+ role="slider"
103
+ tabindex="0"
104
+ aria-valuemin="0"
105
+ aria-valuemax="1"
106
+ aria-valuenow={value}
107
+ aria-label="Media position slider"
108
+ onkeydown={handleScrubberKeyDown}>
109
+ </div>
28
110
  </div>
29
111
  </div>
30
112
 
@@ -40,24 +122,48 @@ const handleSeek = (e) => {
40
122
  }
41
123
  }
42
124
  .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
125
  cursor: pointer;
126
+ position: relative;
127
+ padding: 0.3125rem 0;
128
+ --_seek-bar--container-color: var(--seek-bar--container-color, #b0b0b0);
129
+ --_seek-bar--value-color: var(--seek-bar--value-color, #ffffff);
130
+ --_seek-bar--scrubber-color: var(--seek-bar--scrubber-color, #ffffff);
131
+ --_seek-bar--scrubber-border-color: var(--seek-bar--scrubber-border-color, #b0b0b0);
132
+ --_seek-bar--scrubber-opacity: 0;
133
+ }
134
+ .seek-bar:hover {
135
+ --_seek-bar--scrubber-opacity: 1;
47
136
  }
48
137
  .seek-bar__container {
49
138
  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;
139
+ background: var(--_seek-bar--container-color);
140
+ height: 0.3125rem;
141
+ position: relative;
55
142
  }
56
143
  .seek-bar__value {
57
- background: var(--_seek-bar--front-color);
144
+ background: var(--_seek-bar--value-color);
145
+ border-color: var(--_seek-bar--container-color);
146
+ border-width: 0.0625rem;
58
147
  display: inline-block;
59
148
  height: 100%;
60
149
  }
61
- .seek-bar__value--animate {
62
- transition: width 500ms;
150
+ .seek-bar__scrubber {
151
+ position: absolute;
152
+ top: 50%;
153
+ width: 0.75rem;
154
+ height: 0.75rem;
155
+ background: var(--_seek-bar--scrubber-color);
156
+ border-color: var(--_seek-bar--scrubber-border-color);
157
+ border-width: 0.0625rem;
158
+ border-radius: 50%;
159
+ transform: translate(-50%, -50%);
160
+ z-index: 1;
161
+ opacity: var(--_seek-bar--scrubber-opacity);
162
+ transition: opacity 0.2s ease-in-out;
163
+ }
164
+ .seek-bar__scrubber.is-dragging, .seek-bar__scrubber:focus {
165
+ --_seek-bar--scrubber-opacity: 1;
166
+ }
167
+ .seek-bar__scrubber:focus-visible {
168
+ outline: none;
63
169
  }</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, {}, "">;