@streamscloud/kit 0.1.12 → 0.2.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.
- package/dist/core/toastr/index.d.ts +1 -1
- package/dist/core/toastr/toastr.scss +38 -0
- package/dist/core/toastr/toastr.svelte.d.ts +1 -1
- package/dist/core/toastr/toastr.svelte.js +13 -6
- package/dist/core/toastr/types.d.ts +2 -0
- package/dist/core/transitions/slide-horizontally.js +1 -1
- package/dist/styles/_transitions.scss +24 -0
- package/dist/ui/button/resources/button-base.svelte +1 -1
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +2 -2
- package/dist/ui/dialog/cmp.dialog-container.svelte +12 -12
- package/dist/ui/dialog/dialog-data.d.ts +2 -0
- package/dist/ui/dialog/dialog-mount.d.ts +1 -1
- package/dist/ui/dialog/dialog-mount.js +2 -2
- package/dist/ui/dialog/dialogs.svelte.d.ts +3 -0
- package/dist/ui/dialog/dialogs.svelte.js +21 -2
- package/dist/ui/dialog/index.d.ts +1 -1
- package/dist/ui/dialog/index.js +1 -1
- package/dist/ui/dialog/types.svelte.d.ts +3 -14
- package/dist/ui/dialog/types.svelte.js +3 -18
- package/dist/ui/dropdown/cmp.dropdown.svelte +20 -3
- package/dist/ui/form-group/cmp.form-group-label.svelte +25 -0
- package/dist/ui/form-group/cmp.form-group-label.svelte.d.ts +8 -0
- package/dist/ui/form-group/cmp.form-group-note.svelte +16 -0
- package/dist/ui/form-group/cmp.form-group-note.svelte.d.ts +7 -0
- package/dist/ui/form-group/cmp.form-group.svelte +16 -0
- package/dist/ui/form-group/cmp.form-group.svelte.d.ts +8 -0
- package/dist/ui/form-group/index.d.ts +3 -0
- package/dist/ui/form-group/index.js +3 -0
- package/dist/ui/html-block/cmp.html-block.svelte +112 -0
- package/dist/ui/html-block/cmp.html-block.svelte.d.ts +7 -0
- package/dist/ui/html-block/index.d.ts +1 -0
- package/dist/ui/html-block/index.js +1 -0
- package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte +50 -0
- package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte.d.ts +9 -0
- package/dist/ui/media-viewer-dialog/index.d.ts +14 -0
- package/dist/ui/media-viewer-dialog/index.js +18 -0
- package/dist/ui/media-viewer-dialog/media-viewer-item.svelte +61 -0
- package/dist/ui/media-viewer-dialog/media-viewer-item.svelte.d.ts +7 -0
- package/dist/ui/media-viewer-dialog/types.d.ts +15 -0
- package/dist/ui/media-viewer-dialog/types.js +1 -0
- package/dist/ui/player/carousel/cmp.carousel.svelte +27 -7
- package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +3 -1
- package/dist/ui/player/feed-slider/cmp.feed-slider.svelte +3 -5
- package/dist/ui/player/utils/index.d.ts +1 -0
- package/dist/ui/player/utils/index.js +1 -0
- package/dist/ui/player/{feed-slider → utils}/wheel-gestures-adapter.d.ts +6 -2
- package/dist/ui/player/{feed-slider → utils}/wheel-gestures-adapter.js +22 -13
- package/dist/ui/seek-bar/cmp.seek-bar.svelte +1 -1
- package/dist/ui/video/cmp.video.svelte +20 -9
- package/package.json +17 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts">import { DialogCloseButton } from '../dialog';
|
|
2
|
+
import { Carousel } from '../player/carousel';
|
|
3
|
+
import { default as MediaViewerItem } from './media-viewer-item.svelte';
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
const { controller, data } = $props();
|
|
6
|
+
$effect(() => untrack(() => {
|
|
7
|
+
controller.updateSettings({ closeOnEsc: true, closeOnClickOutside: true });
|
|
8
|
+
controller.updateContainerSettings({ position: 'full-screen' });
|
|
9
|
+
}));
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="media-viewer-dialog" style:--media-viewer-dialog--background--opacity={data.backgroundOpacity}>
|
|
13
|
+
<div class="media-viewer-dialog__close">
|
|
14
|
+
<DialogCloseButton controller={controller} />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="media-viewer-dialog__content">
|
|
18
|
+
{#if data.items.length === 1}
|
|
19
|
+
<MediaViewerItem item={data.items[0]} />
|
|
20
|
+
{:else}
|
|
21
|
+
<Carousel items={data.items} initialIndex={data.index} mode={data.carouselMode ?? 'arrows-with-counts'} wheelNavigation on={{ indexChanged: () => {} }}>
|
|
22
|
+
{#snippet children(item)}
|
|
23
|
+
<MediaViewerItem item={item} />
|
|
24
|
+
{/snippet}
|
|
25
|
+
</Carousel>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<style>.media-viewer-dialog {
|
|
31
|
+
--_media-viewer-dialog--background--opacity: var(--media-viewer-dialog--background--opacity, 0.6);
|
|
32
|
+
--sc-kit--dialog-close-button--color: #ffffff;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
width: 100%;
|
|
36
|
+
height: 100%;
|
|
37
|
+
background: rgba(0, 0, 0, var(--_media-viewer-dialog--background--opacity));
|
|
38
|
+
position: relative;
|
|
39
|
+
padding: 0.625rem;
|
|
40
|
+
}
|
|
41
|
+
.media-viewer-dialog__close {
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: 0.75rem;
|
|
44
|
+
right: 0.75rem;
|
|
45
|
+
z-index: 10;
|
|
46
|
+
}
|
|
47
|
+
.media-viewer-dialog__content {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-height: 0;
|
|
50
|
+
}</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DialogController } from '../dialog';
|
|
2
|
+
import type { MediaViewerData } from './types';
|
|
3
|
+
type Props = {
|
|
4
|
+
controller: DialogController;
|
|
5
|
+
data: MediaViewerData;
|
|
6
|
+
};
|
|
7
|
+
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type Cmp = ReturnType<typeof Cmp>;
|
|
9
|
+
export default Cmp;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DialogResult } from '../dialog';
|
|
2
|
+
import type { MediaViewerData } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Opens a full-screen media viewer dialog for images and videos.
|
|
5
|
+
* Single item renders directly; multiple items display in a carousel with arrow navigation.
|
|
6
|
+
*
|
|
7
|
+
* ### Options
|
|
8
|
+
* - `items` — array of media items to display
|
|
9
|
+
* - `index` — zero-based index of the initially visible item
|
|
10
|
+
* - `backgroundOpacity` — backdrop opacity (default `0.6`)
|
|
11
|
+
* - `carouselMode` — carousel navigation mode (default `'arrows-with-counts'`)
|
|
12
|
+
*/
|
|
13
|
+
export declare const openMediaViewer: (data: MediaViewerData) => Promise<DialogResult<void>>;
|
|
14
|
+
export type { MediaViewerData, MediaViewerItem, MediaViewerItemType } from './types';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Dialogs } from '../dialog';
|
|
2
|
+
import { default as MediaViewerDialog } from './cmp.media-viewer-dialog.svelte';
|
|
3
|
+
/**
|
|
4
|
+
* Opens a full-screen media viewer dialog for images and videos.
|
|
5
|
+
* Single item renders directly; multiple items display in a carousel with arrow navigation.
|
|
6
|
+
*
|
|
7
|
+
* ### Options
|
|
8
|
+
* - `items` — array of media items to display
|
|
9
|
+
* - `index` — zero-based index of the initially visible item
|
|
10
|
+
* - `backgroundOpacity` — backdrop opacity (default `0.6`)
|
|
11
|
+
* - `carouselMode` — carousel navigation mode (default `'arrows-with-counts'`)
|
|
12
|
+
*/
|
|
13
|
+
export const openMediaViewer = (data) => {
|
|
14
|
+
return Dialogs.open({
|
|
15
|
+
view: MediaViewerDialog,
|
|
16
|
+
data
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">import { Image } from '../image';
|
|
2
|
+
import { Video } from '../video';
|
|
3
|
+
const { item } = $props();
|
|
4
|
+
let containerWidth = $state(0);
|
|
5
|
+
let containerHeight = $state(0);
|
|
6
|
+
const fittedSize = $derived.by(() => {
|
|
7
|
+
if (containerWidth === 0 || containerHeight === 0 || item.width === 0 || item.height === 0) {
|
|
8
|
+
return { width: 0, height: 0 };
|
|
9
|
+
}
|
|
10
|
+
const aspectRatio = item.width / item.height;
|
|
11
|
+
const containerRatio = containerWidth / containerHeight;
|
|
12
|
+
if (aspectRatio > containerRatio) {
|
|
13
|
+
const width = containerWidth;
|
|
14
|
+
const height = width / aspectRatio;
|
|
15
|
+
return { width, height };
|
|
16
|
+
}
|
|
17
|
+
const height = containerHeight;
|
|
18
|
+
const width = height * aspectRatio;
|
|
19
|
+
return { width, height };
|
|
20
|
+
});
|
|
21
|
+
const observeResize = (node) => {
|
|
22
|
+
const observer = new ResizeObserver((entries) => {
|
|
23
|
+
const entry = entries[0];
|
|
24
|
+
if (entry) {
|
|
25
|
+
containerWidth = entry.contentRect.width;
|
|
26
|
+
containerHeight = entry.contentRect.height;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
observer.observe(node);
|
|
30
|
+
return {
|
|
31
|
+
destroy: () => observer.disconnect()
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="media-viewer-item" use:observeResize>
|
|
37
|
+
{#if fittedSize.width > 0 && fittedSize.height > 0}
|
|
38
|
+
<div class="media-viewer-item__media" style:width="{fittedSize.width}px" style:height="{fittedSize.height}px">
|
|
39
|
+
{#if item.type === 'image'}
|
|
40
|
+
<Image src={item.url} alt="" />
|
|
41
|
+
{:else}
|
|
42
|
+
<Video src={item.url} poster={item.thumbnailUrl} autoplay="on-appearance" />
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>.media-viewer-item {
|
|
49
|
+
--sc-kit--image--object-fit: contain;
|
|
50
|
+
--sc-kit--video--media-fit: contain;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
}
|
|
57
|
+
.media-viewer-item__media {
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
border-radius: 0.375rem;
|
|
61
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CarouselMode } from '../player/carousel';
|
|
2
|
+
export type MediaViewerItemType = 'image' | 'video';
|
|
3
|
+
export type MediaViewerItem = {
|
|
4
|
+
url: string;
|
|
5
|
+
thumbnailUrl: string | null;
|
|
6
|
+
type: MediaViewerItemType;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
export type MediaViewerData = {
|
|
11
|
+
items: MediaViewerItem[];
|
|
12
|
+
index: number;
|
|
13
|
+
backgroundOpacity?: number;
|
|
14
|
+
carouselMode?: CarouselMode;
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script lang="ts" generics="T">import { Utils, isBrowser } from '../../../core/utils';
|
|
2
2
|
import { Icon } from '../../icon';
|
|
3
|
-
import { TouchSynchronizer } from '../utils';
|
|
3
|
+
import { TouchSynchronizer, createWheelAdapter } from '../utils';
|
|
4
4
|
import { CarouselLocalization } from './carousel-localization';
|
|
5
5
|
import IconChevronLeft from '@fluentui/svg-icons/icons/chevron_left_20_regular.svg?raw';
|
|
6
6
|
import IconChevronRight from '@fluentui/svg-icons/icons/chevron_right_20_regular.svg?raw';
|
|
7
7
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
8
|
-
let { items, mode = 'arrows-with-counts', initialIndex, autoSlideMs = 0, on, dot, children } = $props();
|
|
8
|
+
let { items, mode = 'arrows-with-counts', initialIndex, autoSlideMs = 0, wheelNavigation = false, on, dot, children } = $props();
|
|
9
9
|
const localization = new CarouselLocalization();
|
|
10
10
|
const itemIndices = $derived(items.map((_, index) => index));
|
|
11
11
|
const animationDuration = 300;
|
|
@@ -17,6 +17,20 @@ let slidesRef;
|
|
|
17
17
|
let sliderWidth = $state(0);
|
|
18
18
|
let swipeTransition = $state(0);
|
|
19
19
|
let resizeObserver;
|
|
20
|
+
let wheelAdapter = null;
|
|
21
|
+
const wheelCallbacks = {
|
|
22
|
+
canLoadNext: () => items.length > 1,
|
|
23
|
+
canLoadPrevious: () => items.length > 1,
|
|
24
|
+
onTrigger: (direction) => {
|
|
25
|
+
if (direction === 'next') {
|
|
26
|
+
loadNext();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
loadPrevious();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
getAnimationDurationMs: () => animationDuration
|
|
33
|
+
};
|
|
20
34
|
onMount(() => {
|
|
21
35
|
notifyIndexChanged();
|
|
22
36
|
setTimeout(() => {
|
|
@@ -132,6 +146,12 @@ onMount(() => {
|
|
|
132
146
|
if (isBrowser()) {
|
|
133
147
|
window.addEventListener(`keydown`, onKeyPress);
|
|
134
148
|
}
|
|
149
|
+
if (wheelNavigation) {
|
|
150
|
+
wheelAdapter = createWheelAdapter(slidesRef, {
|
|
151
|
+
axis: 'x',
|
|
152
|
+
cbs: wheelCallbacks
|
|
153
|
+
});
|
|
154
|
+
}
|
|
135
155
|
});
|
|
136
156
|
onDestroy(() => {
|
|
137
157
|
if (resizeObserver) {
|
|
@@ -143,6 +163,7 @@ onDestroy(() => {
|
|
|
143
163
|
window.clearInterval(interval);
|
|
144
164
|
}
|
|
145
165
|
}
|
|
166
|
+
wheelAdapter?.destroy();
|
|
146
167
|
});
|
|
147
168
|
const onKeyPress = Utils.throttle((e) => {
|
|
148
169
|
if (e.key === 'ArrowLeft') {
|
|
@@ -155,7 +176,6 @@ const onKeyPress = Utils.throttle((e) => {
|
|
|
155
176
|
const notifyIndexChanged = () => {
|
|
156
177
|
on?.indexChanged?.(selectedIndex);
|
|
157
178
|
};
|
|
158
|
-
notifyIndexChanged();
|
|
159
179
|
const setIndex = (index) => {
|
|
160
180
|
if (index < 0) {
|
|
161
181
|
animationIndex = 0;
|
|
@@ -174,10 +194,10 @@ const setIndex = (index) => {
|
|
|
174
194
|
}, 600);
|
|
175
195
|
};
|
|
176
196
|
const loadPrevious = Utils.throttle(() => {
|
|
177
|
-
setIndex(
|
|
197
|
+
setIndex(selectedIndex - 1);
|
|
178
198
|
}, animationDuration);
|
|
179
199
|
const loadNext = Utils.throttle(() => {
|
|
180
|
-
setIndex(
|
|
200
|
+
setIndex(selectedIndex + 1);
|
|
181
201
|
}, animationDuration);
|
|
182
202
|
$effect(() => {
|
|
183
203
|
if (previousItems && items !== previousItems) {
|
|
@@ -260,7 +280,7 @@ A horizontal slide carousel with infinite looping, touch swipe support, keyboard
|
|
|
260
280
|
### CSS Custom Properties
|
|
261
281
|
| Property | Description | Default |
|
|
262
282
|
|---|---|---|
|
|
263
|
-
| `--sc-kit--carousel--button-color` | Navigation arrow button background | `
|
|
283
|
+
| `--sc-kit--carousel--button-color` | Navigation arrow button background | `rgba(255, 255, 255, 0.2)` |
|
|
264
284
|
| `--sc-kit--carousel--dot-color` | Dot indicator color (fill when active, border when inactive) | `white` |
|
|
265
285
|
| `--sc-kit--carousel--dot-size` | Dot indicator diameter | `0.5rem` |
|
|
266
286
|
| `--sc-kit--carousel--text-color` | Arrow button icon and counter text color | `white` |
|
|
@@ -269,7 +289,7 @@ A horizontal slide carousel with infinite looping, touch swipe support, keyboard
|
|
|
269
289
|
<style>@charset "UTF-8";
|
|
270
290
|
.carousel {
|
|
271
291
|
/* Public API */
|
|
272
|
-
--_carousel--button-color: var(--sc-kit--carousel--button-color,
|
|
292
|
+
--_carousel--button-color: var(--sc-kit--carousel--button-color, rgba(255, 255, 255, 0.2));
|
|
273
293
|
--_carousel--dot-color: var(--sc-kit--carousel--dot-color, #ffffff);
|
|
274
294
|
--_carousel--dot-size: var(--sc-kit--carousel--dot-size, 0.5rem);
|
|
275
295
|
--_carousel--text-color: var(--sc-kit--carousel--text-color, #ffffff);
|
|
@@ -10,6 +10,8 @@ declare function $$render<T>(): {
|
|
|
10
10
|
mode?: CarouselMode;
|
|
11
11
|
/** Auto-advance interval in ms; 0 disables auto-sliding @default 0 */
|
|
12
12
|
autoSlideMs?: number;
|
|
13
|
+
/** Convert vertical mouse wheel scroll to slide navigation @default false */
|
|
14
|
+
wheelNavigation?: boolean;
|
|
13
15
|
on?: {
|
|
14
16
|
/** Fires after the active slide changes */
|
|
15
17
|
indexChanged: (index: number) => void;
|
|
@@ -46,7 +48,7 @@ interface $$IsomorphicComponent {
|
|
|
46
48
|
* ### CSS Custom Properties
|
|
47
49
|
* | Property | Description | Default |
|
|
48
50
|
* |---|---|---|
|
|
49
|
-
* | `--sc-kit--carousel--button-color` | Navigation arrow button background | `
|
|
51
|
+
* | `--sc-kit--carousel--button-color` | Navigation arrow button background | `rgba(255, 255, 255, 0.2)` |
|
|
50
52
|
* | `--sc-kit--carousel--dot-color` | Dot indicator color (fill when active, border when inactive) | `white` |
|
|
51
53
|
* | `--sc-kit--carousel--dot-size` | Dot indicator diameter | `0.5rem` |
|
|
52
54
|
* | `--sc-kit--carousel--text-color` | Arrow button icon and counter text color | `white` |
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends { id: string }">import { TouchSynchronizer } from '../utils';
|
|
2
|
-
import { createWheelAdapter } from './wheel-gestures-adapter';
|
|
1
|
+
<script lang="ts" generics="T extends { id: string }">import { TouchSynchronizer, createWheelAdapter } from '../utils';
|
|
3
2
|
import { onDestroy, onMount, untrack } from 'svelte';
|
|
4
3
|
let { buffer, on, children } = $props();
|
|
5
4
|
let slidesRef;
|
|
@@ -142,11 +141,10 @@ const wheelCallbacks = {
|
|
|
142
141
|
canLoadNext: () => buffer.canLoadNext,
|
|
143
142
|
canLoadPrevious: () => buffer.canLoadPrevious,
|
|
144
143
|
onTrigger: (direction) => {
|
|
145
|
-
|
|
146
|
-
if (direction > 0) {
|
|
144
|
+
if (direction === 'next') {
|
|
147
145
|
buffer.loadNext();
|
|
148
146
|
}
|
|
149
|
-
else
|
|
147
|
+
else {
|
|
150
148
|
buffer.loadPrevious();
|
|
151
149
|
}
|
|
152
150
|
},
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
export type WheelAdapterAxis = 'x' | 'y';
|
|
2
|
+
export type WheelAdapterDirection = 'next' | 'previous';
|
|
1
3
|
export type WheelAdapterCallbacks = {
|
|
2
4
|
canLoadNext: () => boolean;
|
|
3
5
|
canLoadPrevious: () => boolean;
|
|
4
|
-
onTrigger: (direction:
|
|
6
|
+
onTrigger: (direction: WheelAdapterDirection) => void;
|
|
5
7
|
getAnimationDurationMs: () => number;
|
|
6
8
|
};
|
|
7
9
|
/**
|
|
8
10
|
* Minimal, robust wheel adapter:
|
|
9
|
-
* -
|
|
11
|
+
* - Configurable axis: 'y' reads Y only; 'x' prefers X but remaps Y when X is negligible
|
|
12
|
+
* - EMA over velocity to smooth noisy streams (esp. on Windows)
|
|
10
13
|
* - Cooldown blocks triggers while animation runs
|
|
11
14
|
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
12
15
|
*/
|
|
13
16
|
export declare const createWheelAdapter: (target: HTMLElement, params: {
|
|
17
|
+
axis?: WheelAdapterAxis;
|
|
14
18
|
cbs: WheelAdapterCallbacks;
|
|
15
19
|
}) => {
|
|
16
20
|
destroy(): void;
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { WheelGestures } from 'wheel-gestures';
|
|
3
3
|
/**
|
|
4
4
|
* Minimal, robust wheel adapter:
|
|
5
|
-
* -
|
|
5
|
+
* - Configurable axis: 'y' reads Y only; 'x' prefers X but remaps Y when X is negligible
|
|
6
|
+
* - EMA over velocity to smooth noisy streams (esp. on Windows)
|
|
6
7
|
* - Cooldown blocks triggers while animation runs
|
|
7
8
|
* - Mouse fallback: if velocity is near zero but delta is large, treat as a discrete "kick"
|
|
8
9
|
*/
|
|
9
10
|
export const createWheelAdapter = (target, params) => {
|
|
10
|
-
const { cbs } = params;
|
|
11
|
+
const { axis = 'y', cbs } = params;
|
|
11
12
|
// Tunables
|
|
12
13
|
const PEAK_THRESHOLD = 0.4; // EMA magnitude threshold to consider as a "peak"
|
|
13
14
|
const ACCEL_THRESHOLD = 0.02; // minimal directional EMA rise to count as real acceleration
|
|
@@ -30,41 +31,49 @@ export const createWheelAdapter = (target, params) => {
|
|
|
30
31
|
}, cbs.getAnimationDurationMs() + 100);
|
|
31
32
|
};
|
|
32
33
|
const fire = (direction) => {
|
|
33
|
-
|
|
34
|
-
if ((direction > 0 && !cbs.canLoadNext()) || (direction < 0 && !cbs.canLoadPrevious())) {
|
|
34
|
+
if ((direction === 'next' && !cbs.canLoadNext()) || (direction === 'previous' && !cbs.canLoadPrevious())) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
isAnimating = true;
|
|
38
38
|
cbs.onTrigger(direction);
|
|
39
39
|
startCooldown();
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
const getAxisValues = (axisDelta, axisVelocity) => {
|
|
42
|
+
if (axis === 'y') {
|
|
43
|
+
return [axisDelta[1], axisVelocity[1]];
|
|
44
|
+
}
|
|
45
|
+
// axis === 'x': prefer X, fall back to Y (remap vertical scroll to horizontal)
|
|
46
|
+
if (Math.abs(axisDelta[0]) >= Math.abs(axisDelta[1])) {
|
|
47
|
+
return [axisDelta[0], axisVelocity[0]];
|
|
48
|
+
}
|
|
49
|
+
return [axisDelta[1], axisVelocity[1]];
|
|
50
|
+
};
|
|
51
|
+
wheelGestures.on('wheel', ({ axisDelta, axisVelocity }) => {
|
|
52
|
+
const [delta, velocity] = getAxisValues(axisDelta, axisVelocity);
|
|
44
53
|
// Tracking only: always update EMA and compute signs/acceleration
|
|
45
54
|
previousEmaVelocity = emaVelocity;
|
|
46
|
-
emaVelocity += (
|
|
55
|
+
emaVelocity += (velocity - emaVelocity) * EMA_ALPHA;
|
|
47
56
|
const emaMagnitude = Math.abs(emaVelocity);
|
|
48
|
-
const velocitySign = Math.sign(emaVelocity) || Math.sign(
|
|
57
|
+
const velocitySign = Math.sign(emaVelocity) || Math.sign(velocity);
|
|
49
58
|
const emaAcceleration = emaVelocity - previousEmaVelocity;
|
|
50
59
|
// During animation we only track; no arming, no triggering
|
|
51
60
|
if (isAnimating) {
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
// Path 1: mouse-like discrete kick (platform-agnostic via delta/velocity ratio)
|
|
55
|
-
const absDelta = Math.abs(
|
|
56
|
-
const absVel = Math.abs(
|
|
64
|
+
const absDelta = Math.abs(delta);
|
|
65
|
+
const absVel = Math.abs(velocity);
|
|
57
66
|
const stepRatio = absDelta / Math.max(1e-6, absVel);
|
|
58
67
|
const isMouseLikeKick = absDelta >= MOUSE_DELTA_KICK && stepRatio >= MOUSE_STEP_RATIO_KICK;
|
|
59
68
|
if (isMouseLikeKick) {
|
|
60
|
-
const direction =
|
|
69
|
+
const direction = delta > 0 ? 'next' : 'previous';
|
|
61
70
|
fire(direction);
|
|
62
71
|
return;
|
|
63
72
|
}
|
|
64
73
|
// Path 2: trackpad/inertia via EMA acceleration and peak gating
|
|
65
74
|
const isAcceleratingInDirection = velocitySign !== 0 && emaAcceleration * velocitySign > ACCEL_THRESHOLD;
|
|
66
75
|
if (isAcceleratingInDirection && emaMagnitude > PEAK_THRESHOLD) {
|
|
67
|
-
const direction = velocitySign > 0 ?
|
|
76
|
+
const direction = velocitySign > 0 ? 'next' : 'previous';
|
|
68
77
|
fire(direction);
|
|
69
78
|
}
|
|
70
79
|
});
|
|
@@ -158,7 +158,7 @@ A draggable media seek bar with a track, filled portion, and scrubber handle. Su
|
|
|
158
158
|
transform: translate(-50%, -50%);
|
|
159
159
|
z-index: 1;
|
|
160
160
|
opacity: var(--_seek-bar--scrubber-opacity);
|
|
161
|
-
transition: opacity 0.
|
|
161
|
+
transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
162
162
|
}
|
|
163
163
|
.seek-bar__scrubber.is-dragging, .seek-bar__scrubber:focus {
|
|
164
164
|
--_seek-bar--scrubber-opacity: 1;
|
|
@@ -8,10 +8,11 @@ import IconSpeaker from '@fluentui/svg-icons/icons/speaker_2_20_regular.svg?raw'
|
|
|
8
8
|
import IconSpeakerMute from '@fluentui/svg-icons/icons/speaker_mute_20_regular.svg?raw';
|
|
9
9
|
import { untrack } from 'svelte';
|
|
10
10
|
import { fade } from 'svelte/transition';
|
|
11
|
-
let { src, poster, id = randomNanoid(), controls =
|
|
11
|
+
let { src, poster, id = randomNanoid(), controls = false, autoplay = false, loop = false, inert = false, allowPreloading = false, hideSpeaker = false, hidePlayButton = false, intersectionContainer, scrubberPosition = 'bottom', on } = $props();
|
|
12
12
|
let video = $state(null);
|
|
13
13
|
let videoContainerRef = $state(null);
|
|
14
14
|
let showControlsOnHover = $state(false);
|
|
15
|
+
let showProgressOnHover = $state(false);
|
|
15
16
|
let isVideoPaused = $state(true);
|
|
16
17
|
let percentageCompleted = $state(0);
|
|
17
18
|
let everActivated = $state(false);
|
|
@@ -57,6 +58,7 @@ $effect(() => untrack(() => {
|
|
|
57
58
|
PlaybackManager.unregisterPlayer(id);
|
|
58
59
|
};
|
|
59
60
|
}));
|
|
61
|
+
let latestEntry = null;
|
|
60
62
|
// Intersection observer for lazy loading and autoplay-on-appearance
|
|
61
63
|
$effect(() => {
|
|
62
64
|
if (!videoContainerRef) {
|
|
@@ -69,12 +71,13 @@ $effect(() => {
|
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
const [entry] = entries;
|
|
74
|
+
latestEntry = entry;
|
|
72
75
|
if ((entry.isIntersecting || allowPreloading) && !video.src) {
|
|
73
76
|
video.src = src;
|
|
74
77
|
video.load();
|
|
75
78
|
if (autoplayState === true) {
|
|
76
79
|
const handleCanPlay = () => {
|
|
77
|
-
if (
|
|
80
|
+
if (latestEntry?.isIntersecting) {
|
|
78
81
|
void play();
|
|
79
82
|
autoplayState = false;
|
|
80
83
|
}
|
|
@@ -95,7 +98,7 @@ $effect(() => {
|
|
|
95
98
|
}
|
|
96
99
|
if (entry.intersectionRatio >= 0.6) {
|
|
97
100
|
const handleCanPlay = () => {
|
|
98
|
-
if (
|
|
101
|
+
if (latestEntry && latestEntry.intersectionRatio >= 0.6) {
|
|
99
102
|
void play();
|
|
100
103
|
}
|
|
101
104
|
};
|
|
@@ -246,7 +249,13 @@ const handleSeek = (percent) => {
|
|
|
246
249
|
<img class="video__poster" src={poster} alt="" />
|
|
247
250
|
{/if}
|
|
248
251
|
{#if !controls || !everActivated}
|
|
249
|
-
<div
|
|
252
|
+
<div
|
|
253
|
+
class="video__overlay"
|
|
254
|
+
onclick={togglePlay}
|
|
255
|
+
onkeydown={() => ({})}
|
|
256
|
+
onmouseenter={() => (showControlsOnHover = true)}
|
|
257
|
+
onmouseleave={() => (showControlsOnHover = false)}
|
|
258
|
+
role="none">
|
|
250
259
|
{#if isVideoPaused && !hidePlayButton}
|
|
251
260
|
<button type="button" aria-label="play" class="video__playback-button" onclick={togglePlay} onkeydown={() => ({})}>
|
|
252
261
|
<Icon src={IconPlay} color="white" />
|
|
@@ -256,6 +265,7 @@ const handleSeek = (percent) => {
|
|
|
256
265
|
<Icon src={IconPause} color="white" />
|
|
257
266
|
</button>
|
|
258
267
|
{/if}
|
|
268
|
+
|
|
259
269
|
{#if (showControlsOnHover || MediaVolumeManager.isMuted) && !hideSpeaker}
|
|
260
270
|
<button type="button" aria-label={MediaVolumeManager.isMuted ? 'mute' : 'unmute'} class="video__mute-button" onclick={toggleMute}>
|
|
261
271
|
{#if MediaVolumeManager.isMuted}
|
|
@@ -271,10 +281,10 @@ const handleSeek = (percent) => {
|
|
|
271
281
|
class="video__progress-container"
|
|
272
282
|
class:video__progress-container--top={scrubberPosition === 'top'}
|
|
273
283
|
class:video__progress-container--bottom={scrubberPosition === 'bottom'}
|
|
274
|
-
onmouseenter={() => (
|
|
275
|
-
onmouseleave={() => (
|
|
284
|
+
onmouseenter={() => (showProgressOnHover = true)}
|
|
285
|
+
onmouseleave={() => (showProgressOnHover = false)}
|
|
276
286
|
role="none">
|
|
277
|
-
{#if
|
|
287
|
+
{#if showProgressOnHover || (!showProgressOnHover && isVideoPaused)}
|
|
278
288
|
<div
|
|
279
289
|
class="video__seek-bar"
|
|
280
290
|
transition:fade={{ duration: isVideoPaused ? 0 : 300 }}
|
|
@@ -321,12 +331,12 @@ A full-featured video player with custom overlay controls, play/pause, mute, see
|
|
|
321
331
|
background: var(--_video--background-color);
|
|
322
332
|
}
|
|
323
333
|
.video__playback-button {
|
|
334
|
+
--sc-kit--icon--size: 2.5rem;
|
|
324
335
|
--sc-kit--icon--filter: drop-shadow(1px 1px #000000);
|
|
325
336
|
position: absolute;
|
|
326
337
|
top: 50%;
|
|
327
338
|
left: 50%;
|
|
328
339
|
transform: translate(-50%, -50%);
|
|
329
|
-
font-size: 2em;
|
|
330
340
|
}
|
|
331
341
|
.video__playback-button--pause {
|
|
332
342
|
/* Set 'container-type: inline-size;' to reference container*/
|
|
@@ -337,10 +347,11 @@ A full-featured video player with custom overlay controls, play/pause, mute, see
|
|
|
337
347
|
}
|
|
338
348
|
}
|
|
339
349
|
.video__mute-button {
|
|
350
|
+
--sc-kit--icon--size: 1.25rem;
|
|
351
|
+
--sc-kit--icon--filter: drop-shadow(1px 1px #000000);
|
|
340
352
|
position: absolute;
|
|
341
353
|
top: 0.625em;
|
|
342
354
|
right: 0.625em;
|
|
343
|
-
font-size: 1em;
|
|
344
355
|
z-index: 1;
|
|
345
356
|
}
|
|
346
357
|
.video__poster {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"pack": "npm run build && npm pack",
|
|
18
18
|
"preview": "vite preview",
|
|
19
19
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
|
20
|
+
"check:strict": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings --output machine && eslint . --format codeframe --max-warnings 0 && prettier --check --plugin prettier-plugin-svelte --cache .",
|
|
20
21
|
"lint": "prettier --check --plugin prettier-plugin-svelte . && eslint .",
|
|
21
22
|
"lint-format": "prettier --write --plugin prettier-plugin-svelte . && eslint --fix .",
|
|
22
23
|
"format": "prettier --write --plugin prettier-plugin-svelte ."
|
|
@@ -104,6 +105,10 @@
|
|
|
104
105
|
"types": "./dist/ui/dialog/index.d.ts",
|
|
105
106
|
"svelte": "./dist/ui/dialog/index.js"
|
|
106
107
|
},
|
|
108
|
+
"./ui/form-group": {
|
|
109
|
+
"types": "./dist/ui/form-group/index.d.ts",
|
|
110
|
+
"svelte": "./dist/ui/form-group/index.js"
|
|
111
|
+
},
|
|
107
112
|
"./ui/dropdown": {
|
|
108
113
|
"types": "./dist/ui/dropdown/index.d.ts",
|
|
109
114
|
"svelte": "./dist/ui/dropdown/index.js"
|
|
@@ -112,6 +117,10 @@
|
|
|
112
117
|
"types": "./dist/ui/dynamic-component/index.d.ts",
|
|
113
118
|
"svelte": "./dist/ui/dynamic-component/index.js"
|
|
114
119
|
},
|
|
120
|
+
"./ui/html-block": {
|
|
121
|
+
"types": "./dist/ui/html-block/index.d.ts",
|
|
122
|
+
"svelte": "./dist/ui/html-block/index.js"
|
|
123
|
+
},
|
|
115
124
|
"./ui/icon": {
|
|
116
125
|
"types": "./dist/ui/icon/index.d.ts",
|
|
117
126
|
"svelte": "./dist/ui/icon/index.js"
|
|
@@ -144,6 +153,10 @@
|
|
|
144
153
|
"types": "./dist/ui/media-playback/index.d.ts",
|
|
145
154
|
"svelte": "./dist/ui/media-playback/index.js"
|
|
146
155
|
},
|
|
156
|
+
"./ui/media-viewer-dialog": {
|
|
157
|
+
"types": "./dist/ui/media-viewer-dialog/index.d.ts",
|
|
158
|
+
"svelte": "./dist/ui/media-viewer-dialog/index.js"
|
|
159
|
+
},
|
|
147
160
|
"./ui/player/buttons": {
|
|
148
161
|
"types": "./dist/ui/player/buttons/index.d.ts",
|
|
149
162
|
"svelte": "./dist/ui/player/buttons/index.js"
|
|
@@ -203,6 +216,9 @@
|
|
|
203
216
|
},
|
|
204
217
|
"./styles/theme": {
|
|
205
218
|
"sass": "./dist/styles/_theme.scss"
|
|
219
|
+
},
|
|
220
|
+
"./styles/transitions": {
|
|
221
|
+
"sass": "./dist/styles/_transitions.scss"
|
|
206
222
|
}
|
|
207
223
|
},
|
|
208
224
|
"dependencies": {
|