@streamscloud/kit 0.1.12 → 0.2.0-1772458135351

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 (53) hide show
  1. package/dist/core/toastr/index.d.ts +1 -1
  2. package/dist/core/toastr/toastr.scss +38 -0
  3. package/dist/core/toastr/toastr.svelte.d.ts +1 -1
  4. package/dist/core/toastr/toastr.svelte.js +13 -6
  5. package/dist/core/toastr/types.d.ts +2 -0
  6. package/dist/core/transitions/slide-horizontally.js +1 -1
  7. package/dist/styles/_mixins.scss +2 -2
  8. package/dist/styles/_transitions.scss +24 -0
  9. package/dist/styles/reset.css +1 -1
  10. package/dist/ui/button/resources/button-base.svelte +1 -1
  11. package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +2 -2
  12. package/dist/ui/dialog/cmp.dialog-container.svelte +12 -12
  13. package/dist/ui/dialog/cmp.dialog.svelte +1 -1
  14. package/dist/ui/dialog/dialog-data.d.ts +2 -0
  15. package/dist/ui/dialog/dialog-mount.d.ts +1 -1
  16. package/dist/ui/dialog/dialog-mount.js +2 -2
  17. package/dist/ui/dialog/dialogs.svelte.d.ts +3 -0
  18. package/dist/ui/dialog/dialogs.svelte.js +21 -2
  19. package/dist/ui/dialog/index.d.ts +1 -1
  20. package/dist/ui/dialog/index.js +1 -1
  21. package/dist/ui/dialog/types.svelte.d.ts +3 -14
  22. package/dist/ui/dialog/types.svelte.js +3 -18
  23. package/dist/ui/dropdown/cmp.dropdown.svelte +20 -3
  24. package/dist/ui/form-group/cmp.form-group-label.svelte +25 -0
  25. package/dist/ui/form-group/cmp.form-group-label.svelte.d.ts +8 -0
  26. package/dist/ui/form-group/cmp.form-group-note.svelte +16 -0
  27. package/dist/ui/form-group/cmp.form-group-note.svelte.d.ts +7 -0
  28. package/dist/ui/form-group/cmp.form-group.svelte +16 -0
  29. package/dist/ui/form-group/cmp.form-group.svelte.d.ts +8 -0
  30. package/dist/ui/form-group/index.d.ts +3 -0
  31. package/dist/ui/form-group/index.js +3 -0
  32. package/dist/ui/html-block/cmp.html-block.svelte +112 -0
  33. package/dist/ui/html-block/cmp.html-block.svelte.d.ts +7 -0
  34. package/dist/ui/html-block/index.d.ts +1 -0
  35. package/dist/ui/html-block/index.js +1 -0
  36. package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte +50 -0
  37. package/dist/ui/media-viewer-dialog/cmp.media-viewer-dialog.svelte.d.ts +9 -0
  38. package/dist/ui/media-viewer-dialog/index.d.ts +15 -0
  39. package/dist/ui/media-viewer-dialog/index.js +20 -0
  40. package/dist/ui/media-viewer-dialog/media-viewer-item.svelte +61 -0
  41. package/dist/ui/media-viewer-dialog/media-viewer-item.svelte.d.ts +7 -0
  42. package/dist/ui/media-viewer-dialog/types.d.ts +15 -0
  43. package/dist/ui/media-viewer-dialog/types.js +1 -0
  44. package/dist/ui/player/carousel/cmp.carousel.svelte +27 -7
  45. package/dist/ui/player/carousel/cmp.carousel.svelte.d.ts +3 -1
  46. package/dist/ui/player/feed-slider/cmp.feed-slider.svelte +3 -5
  47. package/dist/ui/player/utils/index.d.ts +1 -0
  48. package/dist/ui/player/utils/index.js +1 -0
  49. package/dist/ui/player/{feed-slider → utils}/wheel-gestures-adapter.d.ts +6 -2
  50. package/dist/ui/player/{feed-slider → utils}/wheel-gestures-adapter.js +22 -13
  51. package/dist/ui/seek-bar/cmp.seek-bar.svelte +1 -1
  52. package/dist/ui/video/cmp.video.svelte +20 -9
  53. package/package.json +17 -1
@@ -0,0 +1,112 @@
1
+ <script lang="ts">let { children } = $props();
2
+ export {};
3
+ </script>
4
+
5
+ <div class="html-block">
6
+ {@render children()}
7
+ </div>
8
+
9
+ <style>.html-block {
10
+ display: contents;
11
+ overflow-wrap: break-word;
12
+ font-size: 0.875em;
13
+ line-height: 1.4;
14
+ font-weight: normal;
15
+ color: light-dark(#2e2e2e, #ffffff);
16
+ }
17
+ .html-block :global(h1) {
18
+ font-size: 1.5em;
19
+ line-height: 1.4;
20
+ font-weight: 600;
21
+ color: light-dark(#2e2e2e, #ffffff);
22
+ }
23
+ .html-block :global(h2) {
24
+ font-size: 1.375em;
25
+ line-height: 1.4;
26
+ font-weight: 600;
27
+ color: light-dark(#2e2e2e, #ffffff);
28
+ }
29
+ .html-block :global(h3) {
30
+ font-size: 1.25em;
31
+ line-height: 1.4;
32
+ font-weight: 600;
33
+ color: light-dark(#2e2e2e, #ffffff);
34
+ }
35
+ .html-block :global(h4) {
36
+ font-size: 1.125em;
37
+ line-height: 1.4;
38
+ font-weight: 500;
39
+ color: light-dark(#2e2e2e, #ffffff);
40
+ }
41
+ .html-block :global(h5) {
42
+ font-size: 1.0625em;
43
+ line-height: 1.4;
44
+ font-weight: 500;
45
+ color: light-dark(#2e2e2e, #ffffff);
46
+ }
47
+ .html-block :global(h6) {
48
+ font-size: 1em;
49
+ line-height: 1.4;
50
+ font-weight: 500;
51
+ color: light-dark(#2e2e2e, #ffffff);
52
+ }
53
+ .html-block :global(a) {
54
+ font-size: 1em;
55
+ line-height: 1.4;
56
+ font-weight: normal;
57
+ color: light-dark(#2e2e2e, #ffffff);
58
+ color: light-dark(#144ab0, #5a8dec);
59
+ }
60
+ .html-block :global(pre) {
61
+ font-family: monospace;
62
+ white-space: pre-wrap;
63
+ margin: 1em 0;
64
+ font-size: 1em;
65
+ line-height: 1;
66
+ font-weight: normal;
67
+ color: light-dark(#2e2e2e, #ffffff);
68
+ }
69
+ .html-block :global(ul) :global(> li),
70
+ .html-block :global(ol) :global(> li) {
71
+ list-style: inherit;
72
+ margin-left: 1em;
73
+ position: relative;
74
+ font-size: 1em;
75
+ line-height: 1.4;
76
+ font-weight: normal;
77
+ color: light-dark(#2e2e2e, #ffffff);
78
+ }
79
+ .html-block :global(ol) {
80
+ list-style-type: decimal;
81
+ }
82
+ .html-block :global(ol) :global(> li > ol) {
83
+ list-style-type: lower-alpha;
84
+ }
85
+ .html-block :global(table) {
86
+ max-width: 100%;
87
+ }
88
+ .html-block :global(table),
89
+ .html-block :global(tbody),
90
+ .html-block :global(thead),
91
+ .html-block :global(tr),
92
+ .html-block :global(th),
93
+ .html-block :global(td) {
94
+ border-width: initial;
95
+ border-style: inherit;
96
+ border-color: inherit;
97
+ }
98
+ .html-block :global(ul:not([style*='list-style-type'])),
99
+ .html-block :global(ul:not([style*='list-style-type']) > li > ul:not([style*='list-style-type'])) {
100
+ list-style-type: none;
101
+ }
102
+ .html-block :global(ul:not([style*='list-style-type']) > li:before),
103
+ .html-block :global(ul:not([style*='list-style-type']) > li > ul:not([style*='list-style-type']) > li:before) {
104
+ content: "-";
105
+ text-indent: -1em;
106
+ position: absolute;
107
+ font-weight: bold;
108
+ }
109
+ .html-block :global(ul[style*='list-style-type'] > li:before),
110
+ .html-block :global(ul > li > ul[style*='list-style-type'] > li:before) {
111
+ content: none;
112
+ }</style>
@@ -0,0 +1,7 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ children: Snippet;
4
+ };
5
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
6
+ type Cmp = ReturnType<typeof Cmp>;
7
+ export default Cmp;
@@ -0,0 +1 @@
1
+ export { default as HtmlBlock } from './cmp.html-block.svelte';
@@ -0,0 +1 @@
1
+ export { default as HtmlBlock } from './cmp.html-block.svelte';
@@ -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,15 @@
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
+ * - `host` — mount target for the dialog element (default `document.body`)
13
+ */
14
+ export declare const openMediaViewer: (data: MediaViewerData, host?: ParentNode) => Promise<DialogResult<void>>;
15
+ export type { MediaViewerData, MediaViewerItem, MediaViewerItemType } from './types';
@@ -0,0 +1,20 @@
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
+ * - `host` — mount target for the dialog element (default `document.body`)
13
+ */
14
+ export const openMediaViewer = (data, host) => {
15
+ return Dialogs.open({
16
+ view: MediaViewerDialog,
17
+ data,
18
+ host
19
+ });
20
+ };
@@ -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,7 @@
1
+ import type { MediaViewerItem } from './types';
2
+ type Props = {
3
+ item: MediaViewerItem;
4
+ };
5
+ declare const MediaViewerItem: import("svelte").Component<Props, {}, "">;
6
+ type MediaViewerItem = ReturnType<typeof MediaViewerItem>;
7
+ export default MediaViewerItem;
@@ -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(--selectedIndex);
197
+ setIndex(selectedIndex - 1);
178
198
  }, animationDuration);
179
199
  const loadNext = Utils.throttle(() => {
180
- setIndex(++selectedIndex);
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 | `neutral-400` |
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, #9ca3af);
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 | `neutral-400` |
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
- // direction: 1 -> next, -1 -> previous
146
- if (direction > 0) {
144
+ if (direction === 'next') {
147
145
  buffer.loadNext();
148
146
  }
149
- else if (direction < 0) {
147
+ else {
150
148
  buffer.loadPrevious();
151
149
  }
152
150
  },
@@ -1 +1,2 @@
1
1
  export { TouchSynchronizer } from './touch-synchronizer';
2
+ export * from './wheel-gestures-adapter';
@@ -1 +1,2 @@
1
1
  export { TouchSynchronizer } from './touch-synchronizer';
2
+ export * from './wheel-gestures-adapter';
@@ -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: number) => void;
6
+ onTrigger: (direction: WheelAdapterDirection) => void;
5
7
  getAnimationDurationMs: () => number;
6
8
  };
7
9
  /**
8
10
  * Minimal, robust wheel adapter:
9
- * - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
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
- * - EMA over axisVelocity[1] to smooth noisy streams (esp. on Windows)
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
- // Respect external guards (e.g., paging boundaries)
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
- wheelGestures.on('wheel', ({ axisDelta: [, axisDeltaY], axisVelocity: [, axisVelocityY] }) => {
42
- const velocityY = axisVelocityY || 0;
43
- const deltaY = axisDeltaY || 0;
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 += (velocityY - emaVelocity) * EMA_ALPHA;
55
+ emaVelocity += (velocity - emaVelocity) * EMA_ALPHA;
47
56
  const emaMagnitude = Math.abs(emaVelocity);
48
- const velocitySign = Math.sign(emaVelocity) || Math.sign(velocityY);
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(deltaY);
56
- const absVel = Math.abs(velocityY);
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 = deltaY > 0 ? 1 : -1; // 1 = next/down, -1 = previous/up
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 ? 1 : -1; // 1 = next/down, -1 = previous/up
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.2s ease-in-out;
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;