ui-svelte 0.2.7 → 0.2.9
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.
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
rootClass?: string;
|
|
32
32
|
slideClass?: string;
|
|
33
33
|
onSlideChange?: (index: number) => void;
|
|
34
|
+
title?: string | Snippet;
|
|
35
|
+
slideWidth?: number;
|
|
36
|
+
gap?: number;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
const {
|
|
@@ -48,7 +51,10 @@
|
|
|
48
51
|
variant = 'default',
|
|
49
52
|
size = 'md',
|
|
50
53
|
indicatorType = 'bar',
|
|
51
|
-
onSlideChange
|
|
54
|
+
onSlideChange,
|
|
55
|
+
title,
|
|
56
|
+
slideWidth: slideWidthProp,
|
|
57
|
+
gap = 0
|
|
52
58
|
}: Props = $props();
|
|
53
59
|
|
|
54
60
|
let currentIndex = $state(0);
|
|
@@ -59,10 +65,13 @@
|
|
|
59
65
|
let currentTranslate = $state(0);
|
|
60
66
|
let prevTranslate = $state(0);
|
|
61
67
|
let autoplayTimer: ReturnType<typeof setTimeout> | null = $state(null);
|
|
68
|
+
let computedSlideWidth = $state(0);
|
|
69
|
+
let computedSlidesPerView = $state(1);
|
|
62
70
|
|
|
63
71
|
const isVertical = $derived(orientation === 'vertical');
|
|
72
|
+
const maxIndex = $derived(Math.max(0, slides.length - Math.floor(computedSlidesPerView)));
|
|
64
73
|
const canGoPrev = $derived(loop || currentIndex > 0);
|
|
65
|
-
const canGoNext = $derived(loop || currentIndex <
|
|
74
|
+
const canGoNext = $derived(loop || currentIndex < maxIndex);
|
|
66
75
|
|
|
67
76
|
const sizeClasses = {
|
|
68
77
|
sm: 'is-sm',
|
|
@@ -71,30 +80,71 @@
|
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
const updateTransform = () => {
|
|
74
|
-
if (!containerEl) return;
|
|
75
|
-
const
|
|
83
|
+
if (!containerEl || !viewportEl) return;
|
|
84
|
+
const viewportSize = isVertical ? viewportEl.offsetHeight : viewportEl.offsetWidth;
|
|
85
|
+
|
|
86
|
+
let slideWidth: number;
|
|
87
|
+
|
|
88
|
+
if (slideWidthProp) {
|
|
89
|
+
slideWidth = slideWidthProp;
|
|
90
|
+
computedSlidesPerView = viewportSize / (slideWidth + gap);
|
|
91
|
+
} else {
|
|
92
|
+
slideWidth = viewportSize;
|
|
93
|
+
computedSlidesPerView = 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
computedSlideWidth = slideWidth;
|
|
97
|
+
|
|
98
|
+
const slideElements = containerEl.querySelectorAll('.carousel-slide');
|
|
99
|
+
slideElements.forEach((el) => {
|
|
100
|
+
(el as HTMLElement).style.width = `${slideWidth}px`;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const offset = -currentIndex * (slideWidth + gap);
|
|
76
104
|
const property = isVertical ? 'translateY' : 'translateX';
|
|
77
|
-
containerEl.style.transform = `${property}(${offset}
|
|
105
|
+
containerEl.style.transform = `${property}(${offset}px)`;
|
|
78
106
|
};
|
|
79
107
|
|
|
80
108
|
const goToSlide = (index: number) => {
|
|
81
|
-
if (index < 0 || index
|
|
109
|
+
if (index < 0 || index > maxIndex) return;
|
|
82
110
|
currentIndex = index;
|
|
83
111
|
updateTransform();
|
|
84
112
|
onSlideChange?.(index);
|
|
85
113
|
resetAutoplay();
|
|
86
114
|
};
|
|
87
115
|
|
|
116
|
+
const goToSlideInstant = (index: number) => {
|
|
117
|
+
if (index < 0 || index > maxIndex) return;
|
|
118
|
+
if (containerEl) {
|
|
119
|
+
containerEl.style.transition = 'none';
|
|
120
|
+
}
|
|
121
|
+
currentIndex = index;
|
|
122
|
+
updateTransform();
|
|
123
|
+
onSlideChange?.(index);
|
|
124
|
+
requestAnimationFrame(() => {
|
|
125
|
+
if (containerEl) {
|
|
126
|
+
containerEl.style.transition = '';
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
resetAutoplay();
|
|
130
|
+
};
|
|
131
|
+
|
|
88
132
|
const goToPrev = () => {
|
|
89
133
|
if (!canGoPrev) return;
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
if (currentIndex === 0) {
|
|
135
|
+
goToSlideInstant(maxIndex);
|
|
136
|
+
} else {
|
|
137
|
+
goToSlide(currentIndex - 1);
|
|
138
|
+
}
|
|
92
139
|
};
|
|
93
140
|
|
|
94
141
|
const goToNext = () => {
|
|
95
142
|
if (!canGoNext) return;
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
if (currentIndex === maxIndex) {
|
|
144
|
+
goToSlideInstant(0);
|
|
145
|
+
} else {
|
|
146
|
+
goToSlide(currentIndex + 1);
|
|
147
|
+
}
|
|
98
148
|
};
|
|
99
149
|
|
|
100
150
|
const startAutoplay = () => {
|
|
@@ -154,7 +204,14 @@
|
|
|
154
204
|
currentTranslate = prevTranslate + percentageMoved;
|
|
155
205
|
|
|
156
206
|
const property = isVertical ? 'translateY' : 'translateX';
|
|
157
|
-
|
|
207
|
+
|
|
208
|
+
if (slideWidthProp) {
|
|
209
|
+
const slideWidth = computedSlideWidth || slideWidthProp;
|
|
210
|
+
const baseOffset = -currentIndex * (slideWidth + gap);
|
|
211
|
+
containerEl.style.transform = `${property}(${baseOffset + diff}px)`;
|
|
212
|
+
} else {
|
|
213
|
+
containerEl.style.transform = `${property}(calc(-${currentIndex * 100}% + ${diff}px))`;
|
|
214
|
+
}
|
|
158
215
|
};
|
|
159
216
|
|
|
160
217
|
const handleDragEnd = () => {
|
|
@@ -222,6 +279,10 @@
|
|
|
222
279
|
}
|
|
223
280
|
};
|
|
224
281
|
|
|
282
|
+
const handleResize = () => {
|
|
283
|
+
updateTransform();
|
|
284
|
+
};
|
|
285
|
+
|
|
225
286
|
onMount(() => {
|
|
226
287
|
tick();
|
|
227
288
|
updateTransform();
|
|
@@ -230,12 +291,14 @@
|
|
|
230
291
|
startAutoplay();
|
|
231
292
|
}
|
|
232
293
|
|
|
294
|
+
window.addEventListener('resize', handleResize);
|
|
233
295
|
document.addEventListener('mousemove', handleMouseMove);
|
|
234
296
|
document.addEventListener('mouseup', handleMouseUp);
|
|
235
297
|
document.addEventListener('keydown', handleKeyDown);
|
|
236
298
|
|
|
237
299
|
return () => {
|
|
238
300
|
stopAutoplay();
|
|
301
|
+
window.removeEventListener('resize', handleResize);
|
|
239
302
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
240
303
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
241
304
|
document.removeEventListener('keydown', handleKeyDown);
|
|
@@ -266,10 +329,53 @@
|
|
|
266
329
|
ontouchstart={handleTouchStart}
|
|
267
330
|
ontouchmove={handleTouchMove}
|
|
268
331
|
ontouchend={handleTouchEnd}
|
|
332
|
+
style="--slides-per-view: {computedSlidesPerView}; --gap: {gap}px;"
|
|
269
333
|
>
|
|
334
|
+
{#if title}
|
|
335
|
+
<div class="carousel-header">
|
|
336
|
+
<div class="carousel-title">
|
|
337
|
+
{#if typeof title === 'string'}
|
|
338
|
+
<h2>{title}</h2>
|
|
339
|
+
{:else}
|
|
340
|
+
{@render title()}
|
|
341
|
+
{/if}
|
|
342
|
+
</div>
|
|
343
|
+
<div class="carousel-header-controls">
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
class="carousel-header-nav is-prev"
|
|
347
|
+
onclick={goToPrev}
|
|
348
|
+
disabled={!canGoPrev}
|
|
349
|
+
aria-label="Previous slide"
|
|
350
|
+
>
|
|
351
|
+
{#if isVertical}
|
|
352
|
+
<Icon icon={ArrowUp24RegularIcon} />
|
|
353
|
+
{:else}
|
|
354
|
+
<Icon icon={ArrowLeft24RegularIcon} />
|
|
355
|
+
{/if}
|
|
356
|
+
</button>
|
|
357
|
+
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
class="carousel-header-nav is-next"
|
|
361
|
+
onclick={goToNext}
|
|
362
|
+
disabled={!canGoNext}
|
|
363
|
+
aria-label="Next slide"
|
|
364
|
+
>
|
|
365
|
+
{#if isVertical}
|
|
366
|
+
<Icon icon={ArrowDown24RegularIcon} />
|
|
367
|
+
{:else}
|
|
368
|
+
<Icon icon={ArrowRight24RegularIcon} />
|
|
369
|
+
{/if}
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
{/if}
|
|
374
|
+
|
|
270
375
|
<div
|
|
271
376
|
class={cn('carousel-container', isDragging && 'is-dragging', isVertical && 'is-vertical')}
|
|
272
377
|
bind:this={containerEl}
|
|
378
|
+
style="gap: {gap}px;"
|
|
273
379
|
>
|
|
274
380
|
{#each slides as slide (slide.id)}
|
|
275
381
|
<div class={cn('carousel-slide', slideClass)}>
|
|
@@ -19,6 +19,9 @@ type Props = {
|
|
|
19
19
|
rootClass?: string;
|
|
20
20
|
slideClass?: string;
|
|
21
21
|
onSlideChange?: (index: number) => void;
|
|
22
|
+
title?: string | Snippet;
|
|
23
|
+
slideWidth?: number;
|
|
24
|
+
gap?: number;
|
|
22
25
|
};
|
|
23
26
|
declare const Carousel: import("svelte").Component<Props, {}, "">;
|
|
24
27
|
type Carousel = ReturnType<typeof Carousel>;
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.carousel-slide {
|
|
39
|
-
@apply shrink-0
|
|
39
|
+
@apply shrink-0 select-none;
|
|
40
40
|
height: 100%;
|
|
41
41
|
user-select: none;
|
|
42
42
|
-webkit-user-select: none;
|
|
@@ -153,4 +153,30 @@
|
|
|
153
153
|
.carousel-counter {
|
|
154
154
|
@apply text-sm text-on-surface/70 text-center font-medium;
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
.carousel-header {
|
|
158
|
+
@apply flex items-center justify-between mb-6 px-1;
|
|
159
|
+
|
|
160
|
+
.carousel-title {
|
|
161
|
+
h2 {
|
|
162
|
+
@apply text-3xl font-bold tracking-tight text-on-surface;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.carousel-header-controls {
|
|
167
|
+
@apply flex items-center gap-3;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.carousel-header-nav {
|
|
171
|
+
@apply flex items-center justify-center w-12 h-12 rounded-full;
|
|
172
|
+
@apply bg-surface text-on-surface transition-all duration-200;
|
|
173
|
+
@apply border border-muted shadow-sm;
|
|
174
|
+
@apply hover:bg-muted hover:scale-105 active:scale-95;
|
|
175
|
+
@apply disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:bg-surface disabled:hover:scale-100;
|
|
176
|
+
|
|
177
|
+
:global(svg) {
|
|
178
|
+
@apply w-6 h-6;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
156
182
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
hideOnScroll?: boolean;
|
|
19
19
|
solidOnScroll?: boolean;
|
|
20
20
|
isSticky?: boolean;
|
|
21
|
+
isFloating?: boolean;
|
|
21
22
|
isBoxed?: boolean;
|
|
22
23
|
variant?:
|
|
23
24
|
| 'primary'
|
|
@@ -47,7 +48,8 @@
|
|
|
47
48
|
isBoxed,
|
|
48
49
|
variant = 'ghost',
|
|
49
50
|
hideOnScroll,
|
|
50
|
-
solidOnScroll = false
|
|
51
|
+
solidOnScroll = false,
|
|
52
|
+
isFloating = false
|
|
51
53
|
}: Props = $props();
|
|
52
54
|
|
|
53
55
|
let headerElement = $state<HTMLElement | null>(null);
|
|
@@ -99,6 +101,7 @@
|
|
|
99
101
|
const shouldBlur = $derived(isBlurred && scroll.isScrolled);
|
|
100
102
|
const shouldShowBorder = $derived(isBordered && (!borderOnScrollOnly || scroll.isScrolled));
|
|
101
103
|
const isTransparent = $derived(solidOnScroll && !scroll.isScrolled);
|
|
104
|
+
const isFloatingActive = $derived(isFloating && !scroll.isScrolled);
|
|
102
105
|
</script>
|
|
103
106
|
|
|
104
107
|
<header
|
|
@@ -111,6 +114,8 @@
|
|
|
111
114
|
isHidden && 'is-hidden',
|
|
112
115
|
isTransparent && 'is-transparent',
|
|
113
116
|
isSticky && 'is-sticky',
|
|
117
|
+
isFloating && 'is-floating-enabled',
|
|
118
|
+
isFloatingActive && 'is-floating',
|
|
114
119
|
rootClass
|
|
115
120
|
)}
|
|
116
121
|
>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@layer components {
|
|
2
2
|
.appbar {
|
|
3
|
-
@apply w-full z-30;
|
|
3
|
+
@apply w-full z-30 transition-all duration-300;
|
|
4
4
|
|
|
5
5
|
&.is-sticky {
|
|
6
6
|
@apply fixed top-0;
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
@apply border-b border-muted;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
&.is-hidden {
|
|
19
18
|
@apply -translate-y-full;
|
|
20
19
|
}
|
|
@@ -23,6 +22,20 @@
|
|
|
23
22
|
@apply bg-transparent border-transparent shadow-none;
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
&.is-floating-enabled {
|
|
26
|
+
@apply left-1/2 -translate-x-1/2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.is-floating {
|
|
30
|
+
@apply mt-4 w-[calc(100%-2rem)] rounded-full shadow-md border border-black/5;
|
|
31
|
+
@apply bg-background/90 backdrop-blur-md;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
&.is-floating-enabled:not(.is-floating) {
|
|
35
|
+
@apply mt-0 w-full rounded-none;
|
|
36
|
+
@apply bg-background/80 backdrop-blur-md border-b border-muted;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
.appbar-content {
|
|
27
40
|
@apply flex flex-nowrap items-center justify-between relative;
|
|
28
41
|
@apply w-full px-2 h-full min-h-12;
|