[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.

add swiper card

Florian (Mar 22, 2025, 3:18 AM +0100) bf464fa5 e2dcc8e0

+336 -4
+18 -2
package-lock.json
··· 1 1 { 2 2 "name": "fuchs", 3 - "version": "0.0.9", 3 + "version": "0.0.11", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "fuchs", 9 - "version": "0.0.9", 9 + "version": "0.0.11", 10 10 "license": "MIT", 11 11 "dependencies": { 12 12 "@atproto/api": "^0.14.7", ··· 17 17 "@threlte/core": "^8.0.1", 18 18 "@threlte/extras": "^9.0.1", 19 19 "@types/three": "^0.173.0", 20 + "@use-gesture/vanilla": "^10.3.1", 20 21 "bits-ui": "^1.3.4", 21 22 "cheerio": "^1.0.0", 22 23 "hls.js": "^1.5.20", ··· 2790 2791 "funding": { 2791 2792 "type": "opencollective", 2792 2793 "url": "https://opencollective.com/typescript-eslint" 2794 + } 2795 + }, 2796 + "node_modules/@use-gesture/core": { 2797 + "version": "10.3.1", 2798 + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", 2799 + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", 2800 + "license": "MIT" 2801 + }, 2802 + "node_modules/@use-gesture/vanilla": { 2803 + "version": "10.3.1", 2804 + "resolved": "https://registry.npmjs.org/@use-gesture/vanilla/-/vanilla-10.3.1.tgz", 2805 + "integrity": "sha512-lT4scGLu59ovA3zmtUonukAGcA0AdOOh+iwNDS05Bsu7Lq9aZToDHhI6D8Q2qvsVraovtsLLYwPrWdG/noMAKw==", 2806 + "license": "MIT", 2807 + "dependencies": { 2808 + "@use-gesture/core": "10.3.1" 2793 2809 } 2794 2810 }, 2795 2811 "node_modules/@webgpu/types": {
+1
package.json
··· 82 82 "@threlte/core": "^8.0.1", 83 83 "@threlte/extras": "^9.0.1", 84 84 "@types/three": "^0.173.0", 85 + "@use-gesture/vanilla": "^10.3.1", 85 86 "bits-ui": "^1.3.4", 86 87 "cheerio": "^1.0.0", 87 88 "hls.js": "^1.5.20",
+1 -1
src/app.html
··· 45 45 </head> 46 46 <body 47 47 data-sveltekit-preload-data="hover" 48 - class="bg-base-50 dark:bg-base-950 motion-safe:transition-colors" 48 + class="bg-base-50 dark:bg-base-950 overflow-x-hidden motion-safe:transition-colors" 49 49 > 50 50 <div style="display: contents">%sveltekit.body%</div> 51 51 </body>
+1 -1
src/docs/site-components/Cards.svelte
··· 76 76 77 77 78 78 <h2 class="text-base-800 dark:text-base-200 mb-4 text-xl font-semibold mt-24">Extra components</h2> 79 - <div class="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 md:gap-8 lg:grid-cols-4"> 79 + <div class="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 lg:gap-8 xl:grid-cols-4"> 80 80 {#each extraComponents as card} 81 81 <div 82 82 class="group relative flex flex-col items-start gap-3 transition-opacity duration-150 hover:opacity-90 md:gap-4"
+49
src/lib/components/extra/countdown/Countdown.svelte
··· 1 + <script lang="ts"> 2 + import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte'; 3 + import { TimerState } from '$lib'; 4 + import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 5 + import type { HTMLAttributes } from 'svelte/elements'; 6 + import { cn } from '$lib/utils.js'; 7 + 8 + let { 9 + timer = $bindable(), 10 + class: className, 11 + ref = $bindable(null), 12 + showHours = false, 13 + showMinutes = true, 14 + showSeconds = true, 15 + ...restProps 16 + }: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & { 17 + timer?: TimerState; 18 + showHours?: boolean; 19 + showMinutes?: boolean; 20 + showSeconds?: boolean; 21 + } = $props(); 22 + 23 + if (!timer) { 24 + timer = new TimerState(1000 * 5); 25 + } 26 + 27 + const ss = $derived(Math.floor(timer.remaining / 1000)); 28 + </script> 29 + 30 + <NumberFlowGroup> 31 + <div 32 + bind:this={ref} 33 + class={cn( 34 + 'text-base-900 dark:text-base-100 flex w-full justify-center text-5xl font-bold', 35 + className 36 + )} 37 + style="font-variant-numeric: tabular-nums;" 38 + {...restProps} 39 + > 40 + 41 + {#if showSeconds} 42 + <NumberFlow 43 + value={ss} 44 + trend={-1} 45 + digits={{ 1: { max: 5 } }} 46 + /> 47 + {/if} 48 + </div> 49 + </NumberFlowGroup>
+1
src/lib/components/extra/stopwatch/StopwatchState.svelte.ts
··· 65 65 } 66 66 this.#dispose(); 67 67 68 + this.status = 'stopped'; 68 69 this.time = new Date(); 69 70 this.startAt = this.time; 70 71 this.elapsed = 0;
+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>
+181
src/lib/components/extra/swiper-cards/CardSwiper.svelte
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { DragGesture, type FullGestureState } from '@use-gesture/vanilla'; 4 + import type { CardData, Direction, SwipeEventData } from '.'; 5 + import Card from './Card.svelte'; 6 + 7 + let container: HTMLElement; 8 + 9 + let card1: HTMLElement | undefined = $state(); 10 + let card2: HTMLElement | undefined = $state(); 11 + let card1Data: CardData = $state({}); 12 + let card2Data: CardData = $state({}); 13 + 14 + let cardIndex = 0; 15 + let topCard: HTMLElement | undefined = $state(); 16 + let currentZ = 100000; 17 + 18 + onMount(async () => { 19 + card1Data = cardData(cardIndex++); 20 + card2Data = cardData(cardIndex++); 21 + 22 + [card1, card2].forEach((el) => { 23 + if (!el) return; 24 + el.style.zIndex = currentZ.toString(); 25 + currentZ--; 26 + 27 + new DragGesture( 28 + el, 29 + (state) => { 30 + ondrag(el, state); 31 + } 32 + ); 33 + }); 34 + 35 + topCard = card1; 36 + container.classList.remove('hidden'); 37 + }); 38 + 39 + const cardSwiped = (el: HTMLElement, velocity: [number, number], movement: [number, number]) => { 40 + el.classList.add('transition-transform', 'duration-300'); 41 + 42 + let direction: Direction = movement[0] > 0 ? 'right' : 'left'; 43 + let data = el === card1 ? card1Data : card2Data; 44 + 45 + onswipe?.({ direction, element: el, data, index: cardIndex - 2 }); 46 + 47 + thresholdPassed = movement[0] > 0 ? 1 : -1; 48 + 49 + let moveOutWidth = document.body.clientWidth; 50 + 51 + let endX = Math.max(Math.abs(velocity[0]) * moveOutWidth, moveOutWidth); 52 + let toX = movement[0] > 0 ? endX : -endX; 53 + let endY = Math.abs(velocity[1]) * moveOutWidth; 54 + let toY = movement[1] > 0 ? endY : -endY; 55 + 56 + let rotate = movement[0] * 0.03 * (movement[1] / 80); 57 + 58 + el.style.transform = `translate(${toX}px, ${toY + movement[1]}px) rotate(${rotate}deg)`; 59 + 60 + setTimeout(() => { 61 + thresholdPassed = 0; 62 + 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 + } 73 + 74 + currentZ--; 75 + el.style.zIndex = currentZ.toString(); 76 + 77 + el.classList.remove('transition-transform', 'duration-300'); 78 + el.style.transform = ''; 79 + }, 350); 80 + }; 81 + 82 + const ondrag = ( 83 + el: HTMLElement, 84 + state: Omit<FullGestureState<'drag'>, 'event'> & { 85 + event: PointerEvent | MouseEvent | TouchEvent | KeyboardEvent; 86 + } 87 + ) => { 88 + let elWidth = el.offsetWidth; 89 + 90 + if (state.active) { 91 + let angle = state.movement[0] * 0.03 * (state.movement[1] / 80); 92 + 93 + // fix movement on a curved path if anchor is set 94 + if (anchor) { 95 + let vec = [state.movement[0], state.movement[1] - anchor]; 96 + let len = Math.sqrt(vec[0] ** 2 + vec[1] ** 2); 97 + vec = [(vec[0] / len) * anchor, (vec[1] / len) * anchor]; 98 + 99 + state.movement[0] = vec[0]; 100 + state.movement[1] = vec[1] + anchor; 101 + } 102 + 103 + el.style.transform = `translate(${state.movement[0]}px, ${state.movement[1]}px)`; 104 + 105 + if (rotate) { 106 + el.style.transform += ` rotate(${angle}deg)`; 107 + } 108 + 109 + if (Math.abs(state.movement[0]) / elWidth > minSwipeDistance) { 110 + thresholdPassed = state.movement[0] > 0 ? 1 : -1; 111 + } else { 112 + thresholdPassed = 0; 113 + } 114 + return; 115 + } 116 + // if dragging is finished 117 + let keep = 118 + Math.abs(state.movement[0]) / elWidth < minSwipeDistance && 119 + Math.abs(state.velocity[0]) < minSwipeVelocity; 120 + 121 + if (keep) { 122 + thresholdPassed = 0; 123 + el.classList.add('transition-transform', 'duration-300'); 124 + el.style.transform = ''; 125 + setTimeout(() => { 126 + el.classList.remove('transition-transform', 'duration-300'); 127 + }, 300); 128 + } else { 129 + cardSwiped(el, state.velocity, state.movement); 130 + } 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 + }; 163 + </script> 164 + 165 + <svelte:body 166 + on:keydown={(e) => { 167 + if (!arrowKeys) return; 168 + if (e.key === 'ArrowLeft') { 169 + swipe('left'); 170 + } else if (e.key === 'ArrowRight') { 171 + swipe('right'); 172 + } 173 + }} 174 + /> 175 + 176 + <div class="isolate h-full w-full touch-none select-none"> 177 + <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} /> 180 + </div> 181 + </div>
+20
src/lib/components/extra/swiper-cards/index.ts
··· 1 + /** 2 + * change this to your own data structure, that each card will use 3 + * then change the Card.svelte file to use your data structure 4 + */ 5 + export type CardData = { 6 + title?: string; 7 + description?: string; 8 + image?: string; 9 + }; 10 + 11 + export type SwipeEventData = { 12 + direction: Direction; 13 + data: CardData; 14 + index: number; 15 + element: HTMLElement; 16 + }; 17 + 18 + export type Direction = 'left' | 'right'; 19 + 20 + export { default as CardSwiper } from './CardSwiper.svelte';
+13
src/routes/components/extras/countdown/+page.svelte
··· 1 + <script lang="ts"> 2 + import type { TimerState } from '$lib'; 3 + import Countdown from '$lib/components/extra/countdown/Countdown.svelte'; 4 + import { onMount } from 'svelte'; 5 + 6 + let timer: TimerState | undefined = $state(); 7 + 8 + onMount(() => { 9 + timer?.start(); 10 + }); 11 + </script> 12 + 13 + <Countdown bind:timer />
+13
src/routes/components/extras/swiper-cards/+page.svelte
··· 1 + <script> 2 + import CardSwiper from "$lib/components/extra/swiper-cards/CardSwiper.svelte"; 3 + 4 + </script> 5 + 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 + })} /> 12 + 13 + </div>