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

small improvements

Florian (Mar 2, 2025, 4:34 PM +0100) f8d8ae66 8d5a89b8

+166 -64
+3 -3
src/routes/+page.svelte
··· 50 50 </script> 51 51 52 52 <ScrollArea type="scroll" class="h-[100dvh] w-screen"> 53 - <div class="flex min-h-screen items-center justify-center p-8"> 53 + <div class="flex min-h-screen items-center justify-center p-4 md:p-8"> 54 54 <div class="h-full w-full max-w-6xl text-black dark:text-white"> 55 55 <div 56 56 class={[ 57 - 'mb-6 flex w-full max-w-6xl items-start justify-between rounded-2xl p-6 md:items-center', 57 + 'mb-4 md:mb-6 flex w-full max-w-6xl items-start justify-between rounded-2xl p-6 md:items-center', 58 58 background 59 59 ]} 60 60 > ··· 71 71 <Github href="https://github.com/flo-bit/ui-kit" /> 72 72 </div> 73 73 </div> 74 - <div class="grid h-full w-full max-w-6xl grid-cols-1 gap-6 md:grid-cols-12 md:grid-rows-5"> 74 + <div class="grid h-full w-full max-w-6xl grid-cols-1 gap-4 md:gap-6 md:grid-cols-12 md:grid-rows-5"> 75 75 <div 76 76 class={[ 77 77 'order-1 col-span-1 row-span-2 flex w-full flex-col items-start justify-between rounded-2xl p-6 md:col-span-5',
+61
src/lib/helper/ThemeWatcher.svelte.ts
··· 1 + export class ThemeWatcher { 2 + type: 'class' | 'mediaQuery' = 'class'; 3 + darkModeMediaQuery: MediaQueryList | null = null; 4 + observer: MutationObserver | null = null; 5 + 6 + observers: ((me: ThemeWatcher) => void)[] = []; 7 + 8 + constructor( 9 + observer: ((me: ThemeWatcher) => void) | null = null, 10 + type: 'class' | 'mediaQuery' = 'class' 11 + ) { 12 + this.type = type; 13 + 14 + if (this.type === 'mediaQuery') { 15 + this.darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 16 + this.darkModeMediaQuery.addEventListener('change', this.updateTheme); 17 + } else { 18 + this.observer = new MutationObserver(() => { 19 + this.updateTheme(); 20 + }); 21 + 22 + this.observer.observe(document.documentElement, { 23 + attributes: true, 24 + attributeFilter: ['class'] 25 + }); 26 + } 27 + 28 + if (observer) { 29 + this.subscribe(observer); 30 + } 31 + } 32 + 33 + isDarkMode() { 34 + if (this.type === 'class') { 35 + return document.documentElement.classList.contains('dark'); 36 + } 37 + 38 + return this.darkModeMediaQuery?.matches ?? false; 39 + } 40 + 41 + getCSSVar(variable: string) { 42 + return getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); 43 + } 44 + 45 + updateTheme() { 46 + this.observers.forEach((observer) => observer(this)); 47 + } 48 + 49 + subscribe(observer: (me: ThemeWatcher) => void) { 50 + this.observers.push(observer); 51 + } 52 + 53 + unsubscribe(observer: (me: ThemeWatcher) => void) { 54 + this.observers = this.observers.filter((o) => o !== observer); 55 + } 56 + 57 + destroy() { 58 + this.observer?.disconnect(); 59 + this.darkModeMediaQuery?.removeEventListener('change', this.updateTheme); 60 + } 61 + }
+3 -2
src/lib/preview/ChatBubblePreview.svelte
··· 2 2 import Box from '$lib/components/box/Box.svelte'; 3 3 import ChatBubble from '$lib/components/chat-bubble/ChatBubble.svelte'; 4 4 import Heading from '$lib/components/heading/Heading.svelte'; 5 - import Image from '$lib/components/image/Image.svelte'; 6 5 6 + import ImageContainer from '$lib/components/image-container/ImageContainer.svelte'; 7 + 7 8 import cute from '$lib/assets/cute.jpg?as=run'; 8 9 </script> 9 10 ··· 30 31 <ChatBubble side="right" variant="secondary"> 31 32 <p>This is me sending you an image</p> 32 33 33 - <Image src={cute} alt="cute cat" class="-mx-1.5 rounded-xl" /> 34 + <ImageContainer src={cute} alt="cute cat" class="-mx-1.5 rounded-xl" /> 34 35 </ChatBubble> 35 36 </Box>
+12 -3
src/lib/preview/ImagePreview.svelte
··· 2 2 import cute from '$lib/assets/cute.jpg?as=run'; 3 3 import cute2 from '$lib/assets/cute2.jpg?as=run'; 4 4 import Subheading from '$lib/components/heading/Subheading.svelte'; 5 - import Image from '$lib/components/image/Image.svelte'; 5 + import ImageContainer from '$lib/components/image-container/ImageContainer.svelte'; 6 6 import Text from '$lib/components/text/Text.svelte'; 7 7 </script> 8 8 9 9 <Subheading class="mb-4">Images</Subheading> 10 10 11 - <Image src={cute} alt="Such a cute animal" /> 11 + <ImageContainer src={cute} alt="Such a cute animal" /> 12 12 13 13 <Text class="mt-4">Overlay theme color</Text> 14 - <Image src={cute2} alt="Another cute animal" useThemeColor={true} class="max-w-64" /> 14 + <ImageContainer src={cute2} alt="Another cute animal" useThemeColor={true} class="max-w-64" /> 15 + 16 + <Text class="mt-4">Show normal image on hover</Text> 17 + <ImageContainer 18 + src={cute2} 19 + alt="Another cute animal" 20 + useThemeColor={true} 21 + class="max-w-64" 22 + showNormalOnHover={true} 23 + />
+4 -2
src/routes/components/+layout.svelte
··· 53 53 54 54 // sort components by label 55 55 components.sort((a, b) => a.label.localeCompare(b.label)); 56 + 57 + console.log(components.length); 56 58 </script> 57 59 58 60 <Navbar hasSidebar> ··· 94 96 </Popover.Root> 95 97 <ThemeToggle /> 96 98 97 - <Github href="https://github.com/flo-bit/ui-kit" class="py-2 pr-2" /> 99 + <Github href="https://github.com/flo-bit/ui-kit" class="py-2 pl-2 pr-4" /> 98 100 </div> 99 101 </Navbar> 100 102 ··· 103 105 </Container> 104 106 105 107 <Sidebar> 106 - <div class="flex flex-col items-start p-4"> 108 + <div class="flex flex-col items-start p-4 mb-8"> 107 109 <Button variant="link" href={base}>Home</Button> 108 110 <Button variant="link" href="{base}/components">All</Button> 109 111 <Button variant="link" href="{base}/components/theme" class="mb-8">Theme</Button>
+15
src/lib/components/image-container/ImageContainer.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '$lib/utils'; 3 + import Image, { type ImageProps } from '$lib/components/image/Image.svelte'; 4 + 5 + let { ref = $bindable(null), containerClasses, ...restProps }: ImageProps = $props(); 6 + </script> 7 + 8 + <Image 9 + bind:ref 10 + {...restProps} 11 + containerClasses={cn( 12 + 'border-base-300 dark:border-base-700 overflow-hidden rounded-2xl border', 13 + containerClasses 14 + )} 15 + />
+61 -28
src/lib/components/image/Image.svelte
··· 1 - <script lang="ts"> 2 - import Img from '@zerodevx/svelte-img'; 1 + <script lang="ts" module> 2 + import { cn } from '$lib/utils'; 3 + import type { HTMLImgAttributes } from 'svelte/elements'; 3 4 import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui'; 4 5 6 + export type ImageProps = WithoutChildrenOrChild< 7 + WithElementRef<HTMLImgAttributes, HTMLImageElement> 8 + > & { 9 + useThemeColor?: boolean; 10 + containerClasses?: string; 11 + blur?: boolean; 12 + showNormalOnHover?: boolean; 13 + alt: string; 14 + }; 15 + </script> 16 + 17 + <script lang="ts"> 18 + import Img from '@zerodevx/svelte-img'; 5 19 import { onMount } from 'svelte'; 6 - import type { HTMLImgAttributes } from 'svelte/elements'; 7 - import { cn } from '$lib/utils'; 8 20 9 21 let loaded = $state(false); 10 22 ··· 16 28 src, 17 29 alt, 18 30 class: className, 19 - imageClasses, 31 + containerClasses, 20 32 useThemeColor = false, 21 - blur = true 22 - }: WithoutChildrenOrChild<WithElementRef<HTMLImgAttributes, HTMLImageElement>> & { 23 - useThemeColor?: boolean; 24 - imageClasses?: string; 25 - blur?: boolean; 26 - } = $props(); 33 + showNormalOnHover = false, 34 + blur = true, 35 + ...restProps 36 + }: ImageProps = $props(); 27 37 </script> 28 38 29 - <div 30 - class={cn( 31 - 'border-base-300 dark:border-base-700 relative overflow-hidden rounded-2xl border', 32 - className 33 - )} 34 - > 39 + <div class={cn('group relative h-fit w-fit', containerClasses)}> 35 40 {#if useThemeColor} 36 - <div class="bg-accent-500/30 pointer-events-none absolute inset-0 z-20 size-full"></div> 41 + <div 42 + class={[ 43 + 'bg-accent-500/30 pointer-events-none absolute inset-0 z-20 size-full', 44 + showNormalOnHover ? 'group-hover:opacity-0 transition-all duration-300 ease-in-out' : '' 45 + ]} 46 + ></div> 37 47 {/if} 38 48 39 - <Img 40 - {src} 41 - bind:ref 42 - {alt} 43 - onload={() => (loaded = true)} 44 - style={useThemeColor ? 'filter: grayscale(100%) brightness(1.2)' : ''} 45 - class={imageClasses} 46 - /> 47 - {#if blur} 49 + {#if typeof src === 'string'} 50 + <img 51 + {src} 52 + bind:this={ref} 53 + {alt} 54 + onload={() => (loaded = true)} 55 + class={cn( 56 + useThemeColor ? 'brightness-125 grayscale' : '', 57 + showNormalOnHover ? 'group-hover:filter-none transition-all duration-300 ease-in-out' : '', 58 + className 59 + )} 60 + {...restProps} 61 + /> 62 + {:else} 63 + <Img 64 + {src} 65 + bind:ref 66 + {alt} 67 + onload={() => (loaded = true)} 68 + class={cn( 69 + useThemeColor ? 'brightness-125 grayscale' : '', 70 + showNormalOnHover ? 'group-hover:filter-none transition-all duration-300 ease-in-out' : '', 71 + className 72 + )} 73 + width={restProps.width ? Number(restProps.width) : undefined} 74 + height={restProps.height ? Number(restProps.height) : undefined} 75 + loading={restProps.loading ?? 'lazy'} 76 + decoding={restProps.decoding ?? 'async'} 77 + sizes={restProps.sizes ?? undefined} 78 + /> 79 + {/if} 80 + {#if !(typeof src === 'string') && blur} 48 81 <div 49 - class="image-blur rounded-2xl pointer-events-none absolute inset-0 backdrop-blur-md data-[loaded=true]:hidden" 82 + class="image-blur pointer-events-none absolute inset-0 rounded-2xl backdrop-blur-md data-[loaded=true]:hidden" 50 83 data-loaded={loaded} 51 84 ></div> 52 85 {/if}
+7 -26
src/lib/components/waveform-audio-player/WaveformAudioPlayer.svelte
··· 3 3 import { onMount } from 'svelte'; 4 4 import WaveSurfer from 'wavesurfer.js'; 5 5 6 + import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 7 + 6 8 let wavesurfer: WaveSurfer | null = $state(null); 7 9 8 10 let { url, darkMode = 'class' }: { ··· 21 23 let isPlaying = $state(false); 22 24 let isLoaded = $state(false); 23 25 24 - let darkModeMediaQuery: MediaQueryList | null = $state(null); 26 + let themeWatcher: ThemeWatcher | null = $state(null); 25 27 26 28 function isDarkMode() { 27 - if (darkMode === 'class') { 28 - return document.documentElement.classList.contains('dark'); 29 - } 30 - 31 - return darkModeMediaQuery?.matches ?? false; 29 + return themeWatcher?.isDarkMode() ?? false; 32 30 } 33 31 34 32 function getCSSVar(variable: string) { 35 - return getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); 33 + return themeWatcher?.getCSSVar(variable) ?? ''; 36 34 } 37 35 38 36 onMount(() => { 39 37 if (!audioContainer) return; 40 38 41 - if (darkMode === 'mediaQuery') { 42 - darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 43 - darkModeMediaQuery.addEventListener('change', updateWaveformColors); 44 - } 39 + themeWatcher = new ThemeWatcher(updateWaveformColors, darkMode); 45 40 46 41 wavesurfer = WaveSurfer.create({ 47 42 container: audioContainer, 48 - waveColor: 'black', 49 - progressColor: 'black', 50 43 cursorWidth: 2, 51 - backend: 'WebAudio', 52 44 height: 16, 53 45 barWidth: 2, 54 46 barGap: 1, ··· 82 74 isPlaying = false; 83 75 }); 84 76 85 - const observer = new MutationObserver(() => { 86 - updateWaveformColors(); 87 - }); 88 - 89 - observer.observe(document.documentElement, { 90 - attributes: true, 91 - attributeFilter: ['class'] 92 - }); 93 - 94 77 return () => { 95 - observer.disconnect(); 96 - 97 78 wavesurfer?.pause(); 98 79 wavesurfer?.destroy(); 99 80 100 - darkModeMediaQuery?.removeEventListener('change', updateWaveformColors); 81 + themeWatcher?.destroy(); 101 82 }; 102 83 }); 103 84