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 < slides.length - 1);
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 offset = -currentIndex * 100;
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 >= slides.length) return;
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
- const newIndex = currentIndex === 0 ? slides.length - 1 : currentIndex - 1;
91
- goToSlide(newIndex);
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
- const newIndex = currentIndex === slides.length - 1 ? 0 : currentIndex + 1;
97
- goToSlide(newIndex);
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
- containerEl.style.transform = `${property}(calc(-${currentIndex * 100}% + ${diff}px))`;
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 w-full select-none;
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
  >
@@ -14,6 +14,7 @@ type Props = {
14
14
  hideOnScroll?: boolean;
15
15
  solidOnScroll?: boolean;
16
16
  isSticky?: boolean;
17
+ isFloating?: boolean;
17
18
  isBoxed?: boolean;
18
19
  variant?: 'primary' | 'secondary' | 'muted' | 'success' | 'info' | 'warning' | 'danger' | 'surface' | 'ghost';
19
20
  };
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-svelte",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "author": {
5
5
  "name": "SappsDev",
6
6
  "email": "info@sappsdev.com"