@dorsk/tsumikit 0.47.0 → 0.48.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/components/molecules/Carousel.svelte +276 -0
- package/dist/components/molecules/Carousel.svelte.d.ts +59 -0
- package/dist/components/molecules/carousel-keyboard.d.ts +44 -0
- package/dist/components/molecules/carousel-keyboard.js +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
/**
|
|
3
|
+
* Ordered full-bleed panels stepped with prev/next, dot indicators, arrow
|
|
4
|
+
* keys and horizontal swipe.
|
|
5
|
+
*
|
|
6
|
+
* ARIA follows the WAI-ARIA carousel pattern in its *group* flavour: the
|
|
7
|
+
* root is a region with `aria-roledescription="carousel"`, every slide is a
|
|
8
|
+
* `group` with `aria-roledescription="slide"` named "N of M", and the dots
|
|
9
|
+
* are plain buttons. The tablist flavour was rejected because it makes the
|
|
10
|
+
* dots the primary navigation and turns each slide into a `tabpanel`, which
|
|
11
|
+
* is the wrong model when prev/next is the main affordance and the panels
|
|
12
|
+
* are unlabelled content rather than named destinations. The slide container
|
|
13
|
+
* is a polite live region while the carousel is not auto-rotating.
|
|
14
|
+
*/
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
import IconButton from './IconButton.svelte';
|
|
17
|
+
import { clampSlide, nextSlideForKey, stepSlide, swipeStep } from './carousel-keyboard.js';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
slides,
|
|
21
|
+
slide,
|
|
22
|
+
index = $bindable(0),
|
|
23
|
+
onchange,
|
|
24
|
+
loop = false,
|
|
25
|
+
autoplay = 0,
|
|
26
|
+
dots = true,
|
|
27
|
+
controls = true,
|
|
28
|
+
counter = false,
|
|
29
|
+
label = 'Carousel',
|
|
30
|
+
class: klass = '',
|
|
31
|
+
style: styleProp = '',
|
|
32
|
+
slideClass = ''
|
|
33
|
+
}: {
|
|
34
|
+
/** One entry per panel. Entries may themselves be snippets when no `slide` renderer is given. */
|
|
35
|
+
slides: T[];
|
|
36
|
+
/** Renders one panel from its entry and position. */
|
|
37
|
+
slide?: Snippet<[T, number]>;
|
|
38
|
+
/** Active panel; bindable. Clamped (or wrapped with `loop`) into range. */
|
|
39
|
+
index?: number;
|
|
40
|
+
onchange?: (index: number) => void;
|
|
41
|
+
/** Wrap from the last panel to the first and back. */
|
|
42
|
+
loop?: boolean;
|
|
43
|
+
/** Milliseconds between automatic advances. `0` (default) disables it. When
|
|
44
|
+
* on, rotation pauses on hover and focus and a play/pause control appears. */
|
|
45
|
+
autoplay?: number;
|
|
46
|
+
dots?: boolean;
|
|
47
|
+
controls?: boolean;
|
|
48
|
+
/** Show an `N / M` position counter. */
|
|
49
|
+
counter?: boolean;
|
|
50
|
+
label?: string;
|
|
51
|
+
class?: string;
|
|
52
|
+
style?: string;
|
|
53
|
+
slideClass?: string;
|
|
54
|
+
} = $props();
|
|
55
|
+
|
|
56
|
+
const count = $derived(slides.length);
|
|
57
|
+
const baseId = `carousel-${Math.random().toString(36).slice(2, 8)}`;
|
|
58
|
+
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const clamped = clampSlide(index, count, false);
|
|
61
|
+
if (clamped !== index) index = clamped;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let rootEl = $state<HTMLElement | null>(null);
|
|
65
|
+
|
|
66
|
+
function go(next: number, focusDot = false) {
|
|
67
|
+
const target = clampSlide(next, count, loop);
|
|
68
|
+
if (target === index) return;
|
|
69
|
+
index = target;
|
|
70
|
+
onchange?.(target);
|
|
71
|
+
if (focusDot) {
|
|
72
|
+
queueMicrotask(() =>
|
|
73
|
+
rootEl?.querySelector<HTMLButtonElement>(`#${baseId}-dot-${target}`)?.focus()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const prev = () => go(stepSlide(index, count, -1, loop));
|
|
78
|
+
const next = () => go(stepSlide(index, count, 1, loop));
|
|
79
|
+
const atStart = $derived(!loop && index <= 0);
|
|
80
|
+
const atEnd = $derived(!loop && index >= count - 1);
|
|
81
|
+
|
|
82
|
+
function onkeydown(e: KeyboardEvent) {
|
|
83
|
+
const tag = (e.target as HTMLElement | null)?.tagName;
|
|
84
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
85
|
+
const target = nextSlideForKey(index, count, e.key, loop);
|
|
86
|
+
if (target === undefined) return;
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const inDots = (e.target as HTMLElement | null)?.closest('.dots') !== null;
|
|
89
|
+
go(target, inDots);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let startX = 0;
|
|
93
|
+
let startY = 0;
|
|
94
|
+
let pointerId: number | null = null;
|
|
95
|
+
function onpointerdown(e: PointerEvent) {
|
|
96
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
97
|
+
pointerId = e.pointerId;
|
|
98
|
+
startX = e.clientX;
|
|
99
|
+
startY = e.clientY;
|
|
100
|
+
}
|
|
101
|
+
function onpointerup(e: PointerEvent) {
|
|
102
|
+
if (e.pointerId !== pointerId) return;
|
|
103
|
+
pointerId = null;
|
|
104
|
+
const step = swipeStep(e.clientX - startX, e.clientY - startY);
|
|
105
|
+
if (step) go(stepSlide(index, count, step, loop));
|
|
106
|
+
}
|
|
107
|
+
function onpointercancel() {
|
|
108
|
+
pointerId = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let playing = $derived(autoplay > 0);
|
|
112
|
+
let hovered = $state(false);
|
|
113
|
+
let focused = $state(false);
|
|
114
|
+
const rotating = $derived(playing && autoplay > 0 && count > 1 && !hovered && !focused);
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (!rotating) return;
|
|
117
|
+
const id = setInterval(() => go(stepSlide(index, count, 1, true)), autoplay);
|
|
118
|
+
return () => clearInterval(id);
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
123
|
+
<section
|
|
124
|
+
bind:this={rootEl}
|
|
125
|
+
class="carousel {klass}"
|
|
126
|
+
style={styleProp}
|
|
127
|
+
aria-roledescription="carousel"
|
|
128
|
+
aria-label={label}
|
|
129
|
+
data-tsu="Carousel"
|
|
130
|
+
{onkeydown}
|
|
131
|
+
onpointerenter={() => (hovered = true)}
|
|
132
|
+
onpointerleave={() => (hovered = false)}
|
|
133
|
+
onfocusin={() => (focused = true)}
|
|
134
|
+
onfocusout={(e) => {
|
|
135
|
+
if (!rootEl?.contains(e.relatedTarget as Node | null)) focused = false;
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
139
|
+
<div
|
|
140
|
+
class="viewport"
|
|
141
|
+
aria-live={rotating ? 'off' : 'polite'}
|
|
142
|
+
{onpointerdown}
|
|
143
|
+
{onpointerup}
|
|
144
|
+
{onpointercancel}
|
|
145
|
+
>
|
|
146
|
+
<div class="track" style:transform="translateX(-{index * 100}%)">
|
|
147
|
+
{#each slides as item, i (i)}
|
|
148
|
+
<div
|
|
149
|
+
class="slide {slideClass}"
|
|
150
|
+
role="group"
|
|
151
|
+
aria-roledescription="slide"
|
|
152
|
+
aria-label="{i + 1} of {count}"
|
|
153
|
+
aria-hidden={i !== index}
|
|
154
|
+
inert={i !== index}
|
|
155
|
+
id="{baseId}-slide-{i}"
|
|
156
|
+
>
|
|
157
|
+
{#if slide}
|
|
158
|
+
{@render slide(item, i)}
|
|
159
|
+
{:else}
|
|
160
|
+
{@render (item as unknown as Snippet)()}
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{#if count > 1 && (controls || dots || counter || autoplay > 0)}
|
|
168
|
+
<div class="bar">
|
|
169
|
+
{#if controls}
|
|
170
|
+
<IconButton icon="chevron-left" label="Previous slide" variant="ghost" box="sm" disabled={atStart} onclick={prev} />
|
|
171
|
+
{/if}
|
|
172
|
+
{#if dots}
|
|
173
|
+
<div class="dots" role="group" aria-label="Choose slide">
|
|
174
|
+
{#each slides as _, i (i)}
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
class="dot"
|
|
178
|
+
class:active={i === index}
|
|
179
|
+
id="{baseId}-dot-{i}"
|
|
180
|
+
aria-label="Slide {i + 1}"
|
|
181
|
+
aria-current={i === index ? 'true' : undefined}
|
|
182
|
+
aria-controls="{baseId}-slide-{i}"
|
|
183
|
+
tabindex={i === index ? 0 : -1}
|
|
184
|
+
onclick={() => go(i)}
|
|
185
|
+
></button>
|
|
186
|
+
{/each}
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
{#if counter}
|
|
190
|
+
<span class="counter">{index + 1} / {count}</span>
|
|
191
|
+
{/if}
|
|
192
|
+
{#if autoplay > 0}
|
|
193
|
+
<IconButton
|
|
194
|
+
icon={playing ? 'pause' : 'play'}
|
|
195
|
+
label={playing ? 'Stop automatic rotation' : 'Start automatic rotation'}
|
|
196
|
+
variant="ghost"
|
|
197
|
+
box="sm"
|
|
198
|
+
pressed={playing}
|
|
199
|
+
onclick={() => (playing = !playing)}
|
|
200
|
+
/>
|
|
201
|
+
{/if}
|
|
202
|
+
{#if controls}
|
|
203
|
+
<IconButton icon="chevron-right" label="Next slide" variant="ghost" box="sm" disabled={atEnd} onclick={next} />
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<style>
|
|
210
|
+
.carousel {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-direction: column;
|
|
213
|
+
gap: var(--sp-3);
|
|
214
|
+
min-width: 0;
|
|
215
|
+
}
|
|
216
|
+
.viewport {
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
border-radius: var(--r-md);
|
|
219
|
+
touch-action: pan-y;
|
|
220
|
+
user-select: none;
|
|
221
|
+
}
|
|
222
|
+
.track {
|
|
223
|
+
display: flex;
|
|
224
|
+
width: 100%;
|
|
225
|
+
transition: transform 0.35s var(--ease);
|
|
226
|
+
}
|
|
227
|
+
.slide {
|
|
228
|
+
flex: 0 0 100%;
|
|
229
|
+
min-width: 0;
|
|
230
|
+
}
|
|
231
|
+
.bar {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: center;
|
|
235
|
+
gap: var(--sp-2);
|
|
236
|
+
}
|
|
237
|
+
.dots {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: var(--sp-2);
|
|
241
|
+
}
|
|
242
|
+
.dot {
|
|
243
|
+
width: 0.5rem;
|
|
244
|
+
height: 0.5rem;
|
|
245
|
+
padding: 0;
|
|
246
|
+
border: 0;
|
|
247
|
+
border-radius: var(--r-pill);
|
|
248
|
+
background: var(--border);
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
transition:
|
|
251
|
+
background 0.12s var(--ease),
|
|
252
|
+
transform 0.12s var(--ease);
|
|
253
|
+
}
|
|
254
|
+
.dot:hover {
|
|
255
|
+
background: var(--text-faint);
|
|
256
|
+
}
|
|
257
|
+
.dot.active {
|
|
258
|
+
background: var(--accent);
|
|
259
|
+
transform: scale(1.25);
|
|
260
|
+
}
|
|
261
|
+
.dot:focus-visible {
|
|
262
|
+
outline: 2px solid var(--accent);
|
|
263
|
+
outline-offset: 2px;
|
|
264
|
+
}
|
|
265
|
+
.counter {
|
|
266
|
+
color: var(--text-muted);
|
|
267
|
+
font-size: var(--fs-sm);
|
|
268
|
+
font-variant-numeric: tabular-nums;
|
|
269
|
+
}
|
|
270
|
+
@media (prefers-reduced-motion: reduce) {
|
|
271
|
+
.track,
|
|
272
|
+
.dot {
|
|
273
|
+
transition: none;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered full-bleed panels stepped with prev/next, dot indicators, arrow
|
|
3
|
+
* keys and horizontal swipe.
|
|
4
|
+
*
|
|
5
|
+
* ARIA follows the WAI-ARIA carousel pattern in its *group* flavour: the
|
|
6
|
+
* root is a region with `aria-roledescription="carousel"`, every slide is a
|
|
7
|
+
* `group` with `aria-roledescription="slide"` named "N of M", and the dots
|
|
8
|
+
* are plain buttons. The tablist flavour was rejected because it makes the
|
|
9
|
+
* dots the primary navigation and turns each slide into a `tabpanel`, which
|
|
10
|
+
* is the wrong model when prev/next is the main affordance and the panels
|
|
11
|
+
* are unlabelled content rather than named destinations. The slide container
|
|
12
|
+
* is a polite live region while the carousel is not auto-rotating.
|
|
13
|
+
*/
|
|
14
|
+
import type { Snippet } from 'svelte';
|
|
15
|
+
declare function $$render<T>(): {
|
|
16
|
+
props: {
|
|
17
|
+
/** One entry per panel. Entries may themselves be snippets when no `slide` renderer is given. */
|
|
18
|
+
slides: T[];
|
|
19
|
+
/** Renders one panel from its entry and position. */
|
|
20
|
+
slide?: Snippet<[T, number]>;
|
|
21
|
+
/** Active panel; bindable. Clamped (or wrapped with `loop`) into range. */
|
|
22
|
+
index?: number;
|
|
23
|
+
onchange?: (index: number) => void;
|
|
24
|
+
/** Wrap from the last panel to the first and back. */
|
|
25
|
+
loop?: boolean;
|
|
26
|
+
/** Milliseconds between automatic advances. `0` (default) disables it. When
|
|
27
|
+
* on, rotation pauses on hover and focus and a play/pause control appears. */
|
|
28
|
+
autoplay?: number;
|
|
29
|
+
dots?: boolean;
|
|
30
|
+
controls?: boolean;
|
|
31
|
+
/** Show an `N / M` position counter. */
|
|
32
|
+
counter?: boolean;
|
|
33
|
+
label?: string;
|
|
34
|
+
class?: string;
|
|
35
|
+
style?: string;
|
|
36
|
+
slideClass?: string;
|
|
37
|
+
};
|
|
38
|
+
exports: {};
|
|
39
|
+
bindings: "index";
|
|
40
|
+
slots: {};
|
|
41
|
+
events: {};
|
|
42
|
+
};
|
|
43
|
+
declare class __sveltets_Render<T> {
|
|
44
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
45
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
46
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
47
|
+
bindings(): "index";
|
|
48
|
+
exports(): {};
|
|
49
|
+
}
|
|
50
|
+
interface $$IsomorphicComponent {
|
|
51
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
52
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
53
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
54
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
55
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
56
|
+
}
|
|
57
|
+
declare const Carousel: $$IsomorphicComponent;
|
|
58
|
+
type Carousel<T> = InstanceType<typeof Carousel<T>>;
|
|
59
|
+
export default Carousel;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp a slide index into `[0, count)`, wrapping when `loop` is set.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} index
|
|
5
|
+
* @param {number} count
|
|
6
|
+
* @param {boolean} [loop]
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
export function clampSlide(index: number, count: number, loop?: boolean): number;
|
|
10
|
+
/**
|
|
11
|
+
* Step from `current` by `delta` slides. Without `loop` the edges absorb the
|
|
12
|
+
* step, so stepping past the last slide stays on it.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} current
|
|
15
|
+
* @param {number} count
|
|
16
|
+
* @param {number} delta
|
|
17
|
+
* @param {boolean} [loop]
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
export function stepSlide(current: number, count: number, delta: number, loop?: boolean): number;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a Carousel navigation key to the slide it selects. Left/Right step by
|
|
23
|
+
* one (wrapping only with `loop`), Home/End jump to the first/last slide.
|
|
24
|
+
* Unhandled keys return `undefined` so the browser keeps its default.
|
|
25
|
+
*
|
|
26
|
+
* @param {number} current
|
|
27
|
+
* @param {number} count
|
|
28
|
+
* @param {string} key
|
|
29
|
+
* @param {boolean} [loop]
|
|
30
|
+
* @returns {number | undefined}
|
|
31
|
+
*/
|
|
32
|
+
export function nextSlideForKey(current: number, count: number, key: string, loop?: boolean): number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Interpret a completed pointer gesture as a slide step. A gesture counts as a
|
|
35
|
+
* horizontal swipe only when it travels at least `threshold` px on the x axis
|
|
36
|
+
* and further on x than on y; anything else is left to the page (vertical
|
|
37
|
+
* scroll, taps). Swiping left reveals the next slide, so the result is +1.
|
|
38
|
+
*
|
|
39
|
+
* @param {number} dx
|
|
40
|
+
* @param {number} dy
|
|
41
|
+
* @param {number} [threshold]
|
|
42
|
+
* @returns {-1 | 0 | 1}
|
|
43
|
+
*/
|
|
44
|
+
export function swipeStep(dx: number, dy: number, threshold?: number): -1 | 0 | 1;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp a slide index into `[0, count)`, wrapping when `loop` is set.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} index
|
|
5
|
+
* @param {number} count
|
|
6
|
+
* @param {boolean} [loop]
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
export function clampSlide(index, count, loop = false) {
|
|
10
|
+
if (count <= 0 || !Number.isFinite(index)) return 0;
|
|
11
|
+
if (loop) return ((Math.trunc(index) % count) + count) % count;
|
|
12
|
+
return Math.min(Math.max(Math.trunc(index), 0), count - 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Step from `current` by `delta` slides. Without `loop` the edges absorb the
|
|
17
|
+
* step, so stepping past the last slide stays on it.
|
|
18
|
+
*
|
|
19
|
+
* @param {number} current
|
|
20
|
+
* @param {number} count
|
|
21
|
+
* @param {number} delta
|
|
22
|
+
* @param {boolean} [loop]
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function stepSlide(current, count, delta, loop = false) {
|
|
26
|
+
return clampSlide(current + delta, count, loop);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Carousel navigation key to the slide it selects. Left/Right step by
|
|
31
|
+
* one (wrapping only with `loop`), Home/End jump to the first/last slide.
|
|
32
|
+
* Unhandled keys return `undefined` so the browser keeps its default.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} current
|
|
35
|
+
* @param {number} count
|
|
36
|
+
* @param {string} key
|
|
37
|
+
* @param {boolean} [loop]
|
|
38
|
+
* @returns {number | undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function nextSlideForKey(current, count, key, loop = false) {
|
|
41
|
+
if (count <= 0) return undefined;
|
|
42
|
+
if (key === 'ArrowRight') return stepSlide(current, count, 1, loop);
|
|
43
|
+
if (key === 'ArrowLeft') return stepSlide(current, count, -1, loop);
|
|
44
|
+
if (key === 'Home') return 0;
|
|
45
|
+
if (key === 'End') return count - 1;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interpret a completed pointer gesture as a slide step. A gesture counts as a
|
|
51
|
+
* horizontal swipe only when it travels at least `threshold` px on the x axis
|
|
52
|
+
* and further on x than on y; anything else is left to the page (vertical
|
|
53
|
+
* scroll, taps). Swiping left reveals the next slide, so the result is +1.
|
|
54
|
+
*
|
|
55
|
+
* @param {number} dx
|
|
56
|
+
* @param {number} dy
|
|
57
|
+
* @param {number} [threshold]
|
|
58
|
+
* @returns {-1 | 0 | 1}
|
|
59
|
+
*/
|
|
60
|
+
export function swipeStep(dx, dy, threshold = 40) {
|
|
61
|
+
if (Math.abs(dx) < threshold || Math.abs(dx) <= Math.abs(dy)) return 0;
|
|
62
|
+
return dx < 0 ? 1 : -1;
|
|
63
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { type Attachment, default as AttachmentList, } from './components/molecu
|
|
|
41
41
|
export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
|
|
42
42
|
export { default as Callout } from './components/molecules/Callout.svelte';
|
|
43
43
|
export { default as CapBar } from './components/molecules/CapBar.svelte';
|
|
44
|
+
export { default as Carousel } from './components/molecules/Carousel.svelte';
|
|
44
45
|
export { default as ChatBubble } from './components/molecules/ChatBubble.svelte';
|
|
45
46
|
export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
|
|
46
47
|
export { default as Composer } from './components/molecules/Composer.svelte';
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export { default as AttachmentList, } from './components/molecules/AttachmentLis
|
|
|
47
47
|
export { default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
|
|
48
48
|
export { default as Callout } from './components/molecules/Callout.svelte';
|
|
49
49
|
export { default as CapBar } from './components/molecules/CapBar.svelte';
|
|
50
|
+
export { default as Carousel } from './components/molecules/Carousel.svelte';
|
|
50
51
|
export { default as ChatBubble } from './components/molecules/ChatBubble.svelte';
|
|
51
52
|
export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
|
|
52
53
|
export { default as Composer } from './components/molecules/Composer.svelte';
|
package/package.json
CHANGED