[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

imrpove swiper

Florian (Mar 22, 2025, 4:26 AM +0100) d05335b8 bf464fa5

+164 -112
src/docs/assets/profiles/0.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/1.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/10.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/11.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/12.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/13.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/2.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/3.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/4.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/5.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/6.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/7.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/8.webp

This is a binary file and will not be displayed.

src/docs/assets/profiles/9.webp

This is a binary file and will not be displayed.

+1 -1
src/lib/components/base/button/Button.svelte
··· 5 5 import { cn } from '$lib/utils'; 6 6 7 7 export const buttonVariants = tv({ 8 - base: 'touch-manipulation hover:cursor-pointer motion-safe:focus-visible:transition-transform focus-visible:outline-2 outline-offset-2 inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-2xl active:scale-95 text-sm font-medium motion-safe:transition-all disabled:pointer-events-none disabled:opacity-50 duration-300 active:duration-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 8 + base: 'touch-manipulation hover:cursor-pointer backdrop-blur-md motion-safe:focus-visible:transition-transform focus-visible:outline-2 outline-offset-2 inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-2xl active:scale-95 text-sm font-medium motion-safe:transition-all disabled:pointer-events-none disabled:opacity-50 duration-300 active:duration-100 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 9 9 variants: { 10 10 variant: { 11 11 primary:
-38
src/lib/components/extra/swiper-cards/Card.svelte
··· 1 - <script lang="ts"> 2 - import { cn } from '$lib'; 3 - 4 - let { 5 - title = '', 6 - description = '', 7 - image = undefined, 8 - class: className, 9 - ref = $bindable() 10 - }: { 11 - title?: string; 12 - description?: string; 13 - image?: string | undefined; 14 - class?: string; 15 - ref?: HTMLElement | null; 16 - } = $props(); 17 - </script> 18 - 19 - <div 20 - class={cn( 21 - 'absolute h-full w-full cursor-grab touch-none rounded-2xl border overflow-hidden border-base-800 dark:border-base-200 bg-white ease-in-out select-none', 22 - className 23 - )} 24 - bind:this={ref} 25 - > 26 - {#key image} 27 - {#if image} 28 - <img class="h-full w-full rounded-2xl object-cover" src={image} alt={title} loading="eager" /> 29 - {/if} 30 - {/key} 31 - <div class="absolute inset-0 rounded-b-xl bg-gradient-to-t from-white/80 via-transparent"></div> 32 - <div class="absolute bottom-0 flex w-full justify-start p-8"> 33 - <div class="flex flex-col"> 34 - <h3 class="pb-4 text-3xl font-semibold">{title}</h3> 35 - <p>{description}</p> 36 - </div> 37 - </div> 38 - </div>
+118 -64
src/lib/components/extra/swiper-cards/CardSwiper.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 2 + // TODO: fix types 3 + 4 + import { onMount, type Snippet } from 'svelte'; 3 5 import { DragGesture, type FullGestureState } from '@use-gesture/vanilla'; 4 6 import type { CardData, Direction, SwipeEventData } from '.'; 5 - import Card from './Card.svelte'; 7 + import { Image } from '$lib'; 8 + 9 + let { 10 + card = DefaultCard, 11 + cardData, 12 + onswipe, 13 + swipe = $bindable(), 14 + 15 + minSwipeDistance = 0.5, 16 + minSwipeVelocity = 0.5, 17 + arrowKeys = true, 18 + thresholdPassed = $bindable(0), 19 + anchor = null, 20 + rotate = true, 21 + cardCount = 10 22 + }: { 23 + card?: Snippet<[CardData]>; 24 + 25 + cardData: (index: number) => CardData; 26 + onswipe?: (cardInfo: SwipeEventData) => void; 27 + swipe?: (direction: Direction) => void; 28 + 29 + minSwipeDistance?: number; 30 + minSwipeVelocity?: number; 31 + arrowKeys?: boolean; 32 + thresholdPassed?: number; 33 + anchor?: number | null; 34 + rotate?: boolean; 35 + cardCount?: number; 36 + } = $props(); 37 + 38 + swipe = (direction: Direction = 'right') => { 39 + if (thresholdPassed !== 0) return; 40 + 41 + let dir = direction === 'left' ? -1 : 1; 42 + 43 + if (!topCard) throw new Error('No top card found'); 44 + cardSwiped(topCard, [dir, 0.1], [dir, 1]); 45 + }; 6 46 7 47 let container: HTMLElement; 8 48 9 - let card1: HTMLElement | undefined = $state(); 10 - let card2: HTMLElement | undefined = $state(); 11 - let card1Data: CardData = $state({}); 12 - let card2Data: CardData = $state({}); 13 - 14 49 let cardIndex = 0; 15 50 let topCard: HTMLElement | undefined = $state(); 51 + let topCardIndex = 0; 52 + 16 53 let currentZ = 100000; 17 54 18 - onMount(async () => { 19 - card1Data = cardData(cardIndex++); 20 - card2Data = cardData(cardIndex++); 55 + let cards: HTMLElement[] = $state(new Array(cardCount)); 56 + let cardsData: CardData[] = $state([]); 57 + 58 + onMount(() => { 59 + for (let i = 0; i < cardCount; i++) { 60 + cardsData.push(cardData(cardIndex++)); 61 + cards.push(); 62 + } 63 + 64 + let gestures: DragGesture[] = []; 21 65 22 - [card1, card2].forEach((el) => { 66 + cards.forEach((el) => { 23 67 if (!el) return; 68 + 24 69 el.style.zIndex = currentZ.toString(); 25 70 currentZ--; 26 71 27 - new DragGesture( 28 - el, 29 - (state) => { 30 - ondrag(el, state); 31 - } 32 - ); 72 + let gesture = new DragGesture(el, (state) => { 73 + ondrag(el, state); 74 + }); 75 + 76 + gestures.push(gesture); 33 77 }); 34 78 35 - topCard = card1; 79 + topCardIndex = 0; 80 + topCard = cards[topCardIndex]; 81 + 36 82 container.classList.remove('hidden'); 83 + 84 + return () => { 85 + gestures.forEach((gesture) => gesture.destroy()); 86 + }; 37 87 }); 38 88 39 89 const cardSwiped = (el: HTMLElement, velocity: [number, number], movement: [number, number]) => { 40 90 el.classList.add('transition-transform', 'duration-300'); 91 + let index = cards.indexOf(el); 41 92 42 93 let direction: Direction = movement[0] > 0 ? 'right' : 'left'; 43 - let data = el === card1 ? card1Data : card2Data; 94 + let data = cardsData[index]; 44 95 45 96 onswipe?.({ direction, element: el, data, index: cardIndex - 2 }); 46 97 ··· 57 108 58 109 el.style.transform = `translate(${toX}px, ${toY + movement[1]}px) rotate(${rotate}deg)`; 59 110 60 - setTimeout(() => { 111 + setTimeout(async () => { 61 112 thresholdPassed = 0; 62 113 63 - // move card back to start position at bottom of stack and update data 64 - if (el === card1) { 65 - card1Data = {}; 66 - card1Data = cardData(cardIndex++); 67 - topCard = card2; 68 - } else { 69 - card2Data = {}; 70 - card2Data = cardData(cardIndex++); 71 - topCard = card1; 72 - } 114 + cardsData[index] = cardData(cardIndex++); 115 + 116 + // next card 117 + topCardIndex = (topCardIndex + 1) % cardCount; 118 + topCard = cards[topCardIndex]; 73 119 74 120 currentZ--; 75 121 el.style.zIndex = currentZ.toString(); ··· 128 174 } else { 129 175 cardSwiped(el, state.velocity, state.movement); 130 176 } 131 - }; 132 - 133 - let { 134 - cardData, 135 - minSwipeDistance = 0.5, 136 - minSwipeVelocity = 0.5, 137 - arrowKeys = true, 138 - thresholdPassed = $bindable(0), 139 - anchor = null, 140 - onswipe, 141 - swipe = $bindable(), 142 - rotate = true, 143 - }: { 144 - cardData: (index: number) => CardData; 145 - minSwipeDistance?: number; 146 - minSwipeVelocity?: number; 147 - arrowKeys?: boolean; 148 - thresholdPassed?: number; 149 - anchor?: number | null; 150 - onswipe?: (cardInfo: SwipeEventData) => void; 151 - swipe?: (direction: Direction) => void; 152 - rotate?: boolean; 153 - } = $props(); 154 - 155 - swipe = (direction: Direction = 'right') => { 156 - if (thresholdPassed !== 0) return; 157 - 158 - let dir = direction === 'left' ? -1 : 1; 159 - 160 - if (!topCard) throw new Error('No top card found'); 161 - cardSwiped(topCard, [dir, 0.1], [dir, 1]); 162 177 }; 163 178 </script> 164 179 165 180 <svelte:body 166 - on:keydown={(e) => { 181 + onkeydown={(e) => { 167 182 if (!arrowKeys) return; 168 183 if (e.key === 'ArrowLeft') { 169 184 swipe('left'); ··· 173 188 }} 174 189 /> 175 190 191 + {#snippet DefaultCard({ image, title, description }: CardData)} 192 + <div 193 + class="border-base-400 dark:border-base-600 bg-base-50 dark:bg-base-900 relative h-full w-full overflow-hidden rounded-2xl border" 194 + > 195 + {#key image} 196 + {#if image} 197 + <Image 198 + containerClasses="absolute inset-0 h-full w-full rounded-2xl" 199 + src={image} 200 + alt={title ?? ''} 201 + loading="eager" 202 + class="h-full w-full object-cover" 203 + /> 204 + {/if} 205 + {/key} 206 + <div 207 + class="from-base-50/80 dark:from-base-950/80 absolute inset-0 rounded-b-xl bg-gradient-to-t via-transparent" 208 + ></div> 209 + <div class="absolute bottom-0 flex w-full justify-start px-3 sm:px-12 py-16"> 210 + <div class="flex flex-col"> 211 + <h3 class="text-base-900 dark:text-base-50 pb-2 text-3xl font-bold">{title}</h3> 212 + <p class="text-base-800 dark:text-base-200 text-sm">{description}</p> 213 + </div> 214 + </div> 215 + </div> 216 + {/snippet} 217 + 176 218 <div class="isolate h-full w-full touch-none select-none"> 177 219 <div class="relative z-0 hidden h-full w-full" bind:this={container}> 178 - <Card bind:ref={card1} {...card1Data} /> 179 - <Card bind:ref={card2} {...card2Data} /> 220 + {#each cards as _, i} 221 + <div 222 + bind:this={cards[i]} 223 + class="absolute h-full w-full cursor-grab touch-none overflow-hidden ease-in-out select-none" 224 + > 225 + {#if cardsData[i]} 226 + {@render card({ 227 + image: cardsData[i].image, 228 + title: cardsData[i].title, 229 + description: cardsData[i].description 230 + })} 231 + {/if} 232 + </div> 233 + {/each} 180 234 </div> 181 235 </div>
+45 -9
src/routes/components/extras/swiper-cards/+page.svelte
··· 1 - <script> 2 - import CardSwiper from "$lib/components/extra/swiper-cards/CardSwiper.svelte"; 1 + <script lang="ts"> 2 + import { Button, cn } from '$lib'; 3 + import { type CardData } from '$lib/components/extra/swiper-cards'; 4 + import CardSwiper from '$lib/components/extra/swiper-cards/CardSwiper.svelte'; 5 + 6 + import profile1 from '$docs/assets/profiles/0.webp?as=run'; 7 + import profile2 from '$docs/assets/profiles/1.webp?as=run'; 8 + import profile3 from '$docs/assets/profiles/2.webp?as=run'; 9 + import profile4 from '$docs/assets/profiles/3.webp?as=run'; 10 + import profile5 from '$docs/assets/profiles/4.webp?as=run'; 11 + import profile6 from '$docs/assets/profiles/5.webp?as=run'; 12 + import profile7 from '$docs/assets/profiles/6.webp?as=run'; 13 + import profile8 from '$docs/assets/profiles/7.webp?as=run'; 14 + import profile9 from '$docs/assets/profiles/8.webp?as=run'; 15 + import profile10 from '$docs/assets/profiles/9.webp?as=run'; 16 + import Image from '$lib/components/base/image/Image.svelte'; 17 + 18 + const profiles = [profile1, profile2, profile3, profile4, profile5, profile6, profile7, profile8, profile9, profile10]; 3 19 20 + let swipe: (direction: 'left' | 'right') => void; 4 21 </script> 5 22 6 - <div class="w-full h-[70vh]"> 7 - <CardSwiper cardData={(i) => ({ 8 - title: `Card ${i}`, 9 - description: `Card ${i} description`, 10 - image: `https://picsum.photos/200/1000?${i}` 11 - })} /> 23 + <div class="h-[70vh] w-full relative"> 24 + <CardSwiper 25 + cardData={(i) => ({ 26 + title: `Card ${i}`, 27 + description: `Card ${i} description`, 28 + image: profiles[i % profiles.length] 29 + })} 30 + bind:swipe 31 + > 32 + </CardSwiper> 12 33 13 - </div> 34 + <div class="w-full flex justify-between absolute bottom-2 px-2"> 35 + <Button onclick={() => swipe('left')} size="iconLg"> 36 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor" class="size-6"> 37 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 38 + </svg> 39 + 40 + 41 + </Button> 42 + <Button onclick={() => swipe('right')} size="iconLg"> 43 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6"> 44 + <path d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z" /> 45 + </svg> 46 + 47 + </Button> 48 + </div> 49 + </div>