[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 waveform audio player

Florian (Mar 2, 2025, 1:19 AM +0100) ebeaa0a4 05bae241

+219 -7
+1 -1
README.md
··· 68 68 Some components might need additional dependencies, just install once you get an error, or install all of them at once: 69 69 70 70 ```bash 71 - npm install bits-ui tailwind-variants @tailwindcss/forms @tailwindcss/typography layerchart plyr svelte-sonner tailwindcss-animate three @threlte/extras @threlte/core @types/three @zerodevx/svelte-img @number-flow/svelte 71 + npm install bits-ui tailwind-variants @tailwindcss/forms @tailwindcss/typography layerchart plyr svelte-sonner tailwindcss-animate three @threlte/extras @threlte/core @types/three @zerodevx/svelte-img @number-flow/svelte wavesurfer.js 72 72 ``` 73 73 74 74 again tailwind plugins should be added to your `app.css`
+7 -1
package-lock.json
··· 19 19 "svelte-sonner": "^0.3.28", 20 20 "tailwind-variants": "^0.3.1", 21 21 "tailwindcss-animate": "^1.0.7", 22 - "three": "^0.173.0" 22 + "three": "^0.173.0", 23 + "wavesurfer.js": "^7.9.1" 23 24 }, 24 25 "devDependencies": { 25 26 "@eslint/compat": "^1.2.5", ··· 5904 5905 "optional": true 5905 5906 } 5906 5907 } 5908 + }, 5909 + "node_modules/wavesurfer.js": { 5910 + "version": "7.9.1", 5911 + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.9.1.tgz", 5912 + "integrity": "sha512-+pG8X9c9BrfAW8KR54OPzZcAj/57sOL08He/tc6/7Mt6NCX3IfDFwexQxXVTILggGZUOUk7tFKfny32yGrysKA==" 5907 5913 }, 5908 5914 "node_modules/webgl-sdf-generator": { 5909 5915 "version": "1.1.1",
+2 -1
package.json
··· 49 49 "svelte-sonner": "^0.3.28", 50 50 "tailwind-variants": "^0.3.1", 51 51 "tailwindcss-animate": "^1.0.7", 52 - "three": "^0.173.0" 52 + "three": "^0.173.0", 53 + "wavesurfer.js": "^7.9.1" 53 54 } 54 55 }
static/audio/music.mp3

This is a binary file and will not be displayed.

+3 -3
src/lib/preview/SelectTheme.svelte
··· 14 14 import ColorSelect from '$lib/components/color-select/ColorSelect.svelte'; 15 15 import Subheading from '$lib/components/heading/Subheading.svelte'; 16 16 import Text from '$lib/components/text/Text.svelte'; 17 - import { onMount, tick } from 'svelte'; 17 + import { onMount } from 'svelte'; 18 18 19 19 let accentColors = [ 20 20 { class: 'bg-red-500', label: 'red' }, ··· 72 72 bind:selected={accentColor} 73 73 colors={accentColors} 74 74 onselected={(color, previous) => { 75 - document.body.classList.remove(previous.label.toLowerCase()); 76 - document.body.classList.add(color.label.toLowerCase()); 75 + document.documentElement.classList.remove(previous.label.toLowerCase()); 76 + document.documentElement.classList.add(color.label.toLowerCase()); 77 77 78 78 localStorage.setItem('accentColor', JSON.stringify(color.label)); 79 79
+9
src/lib/preview/WaveformAudioPlayerPreview.svelte
··· 1 + <script> 2 + import { base } from '$app/paths'; 3 + import Subheading from '$lib/components/heading/Subheading.svelte'; 4 + import WaveformAudioPlayer from '$lib/components/waveform-audio-player/WaveformAudioPlayer.svelte'; 5 + </script> 6 + 7 + <Subheading class="mb-4">Waveform Audio Player</Subheading> 8 + 9 + <WaveformAudioPlayer url={base + '/audio/music.mp3'} />
+2 -1
src/routes/components/+layout.svelte
··· 47 47 { label: 'Modal', href: 'modal' }, 48 48 { label: 'Timer', href: 'timer' }, 49 49 { label: 'Number Input', href: 'number-input' }, 50 - { label: 'Social Icons', href: 'social-icons' } 50 + { label: 'Social Icons', href: 'social-icons' }, 51 + { label: 'Waveform Audio Player', href: 'waveform-audio-player' } 51 52 ]; 52 53 53 54 // sort components by label
+4
src/routes/components/+page.svelte
··· 33 33 import NumberInputPreview from '$lib/preview/NumberInputPreview.svelte'; 34 34 import SocialIconsPreview from '$lib/preview/SocialIconsPreview.svelte'; 35 35 import TimerPreview from '$lib/preview/TimerPreview.svelte'; 36 + import WaveformAudioPlayerPreview from '$lib/preview/WaveformAudioPlayerPreview.svelte'; 36 37 </script> 37 38 38 39 <Heading class="mb-8">All Components</Heading> ··· 136 137 </div> 137 138 <div class="flex flex-col items-start gap-4 py-8"> 138 139 <VideoPlayerPreview></VideoPlayerPreview> 140 + </div> 141 + <div class="flex flex-col items-start gap-4 py-8"> 142 + <WaveformAudioPlayerPreview></WaveformAudioPlayerPreview> 139 143 </div> 140 144 </div>
+186
src/lib/components/waveform-audio-player/WaveformAudioPlayer.svelte
··· 1 + <script lang="ts"> 2 + import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte'; 3 + import { onMount } from 'svelte'; 4 + import WaveSurfer from 'wavesurfer.js'; 5 + 6 + let wavesurfer: WaveSurfer | null = $state(null); 7 + 8 + let { url, darkMode = 'class' }: { 9 + url: string; 10 + darkMode?: 'class' | 'mediaQuery'; 11 + } = $props(); 12 + 13 + let audioContainer: HTMLElement | null = $state(null); 14 + 15 + let audioDuration = $state(0); 16 + let currentTime = $state(0); 17 + let remainingTime = $derived(Math.round(audioDuration - currentTime)); 18 + let remainingSeconds = $derived(remainingTime % 60); 19 + let remainingMinutes = $derived(Math.floor(remainingTime / 60)); 20 + 21 + let isPlaying = $state(false); 22 + let isLoaded = $state(false); 23 + 24 + let darkModeMediaQuery: MediaQueryList | null = $state(null); 25 + 26 + function isDarkMode() { 27 + if (darkMode === 'class') { 28 + return document.documentElement.classList.contains('dark'); 29 + } 30 + 31 + return darkModeMediaQuery?.matches ?? false; 32 + } 33 + 34 + function getCSSVar(variable: string) { 35 + return getComputedStyle(document.documentElement).getPropertyValue(variable).trim(); 36 + } 37 + 38 + onMount(() => { 39 + if (!audioContainer) return; 40 + 41 + if (darkMode === 'mediaQuery') { 42 + darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 43 + darkModeMediaQuery.addEventListener('change', updateWaveformColors); 44 + } 45 + 46 + wavesurfer = WaveSurfer.create({ 47 + container: audioContainer, 48 + waveColor: 'black', 49 + progressColor: 'black', 50 + cursorWidth: 2, 51 + backend: 'WebAudio', 52 + height: 16, 53 + barWidth: 2, 54 + barGap: 1, 55 + barRadius: 2, 56 + dragToSeek: { 57 + debounceTime: 100 58 + }, 59 + fillParent: true, 60 + normalize: true, 61 + hideScrollbar: true 62 + }); 63 + 64 + updateWaveformColors(); 65 + 66 + wavesurfer?.load(url); 67 + 68 + wavesurfer?.on('ready', (duration: number) => { 69 + audioDuration = duration; 70 + isLoaded = true; 71 + }); 72 + 73 + wavesurfer?.on('timeupdate', (time: number) => { 74 + currentTime = time; 75 + }); 76 + 77 + wavesurfer?.on('play', () => { 78 + isPlaying = true; 79 + }); 80 + 81 + wavesurfer?.on('pause', () => { 82 + isPlaying = false; 83 + }); 84 + 85 + const observer = new MutationObserver(() => { 86 + updateWaveformColors(); 87 + }); 88 + 89 + observer.observe(document.documentElement, { 90 + attributes: true, 91 + attributeFilter: ['class'] 92 + }); 93 + 94 + return () => { 95 + observer.disconnect(); 96 + 97 + wavesurfer?.pause(); 98 + wavesurfer?.destroy(); 99 + 100 + darkModeMediaQuery?.removeEventListener('change', updateWaveformColors); 101 + }; 102 + }); 103 + 104 + function updateWaveformColors() { 105 + wavesurfer?.setOptions({ 106 + waveColor: isDarkMode() ? getCSSVar('--color-accent-950') : getCSSVar('--color-accent-200'), 107 + progressColor: isDarkMode() ? getCSSVar('--color-accent-500') : getCSSVar('--color-accent-500') 108 + }); 109 + } 110 + 111 + function playAudio() { 112 + wavesurfer?.play(); 113 + } 114 + 115 + function pauseAudio() { 116 + wavesurfer?.pause(); 117 + } 118 + </script> 119 + 120 + <div class="relative flex h-24 w-full items-center justify-center gap-4 overflow-hidden"> 121 + <button 122 + onclick={() => { 123 + isPlaying ? pauseAudio() : playAudio(); 124 + }} 125 + class="bg-accent-600 hover:bg-accent-500 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-30 md:size-14" 126 + > 127 + {#if isPlaying} 128 + <svg 129 + xmlns="http://www.w3.org/2000/svg" 130 + viewBox="0 0 24 24" 131 + fill="currentColor" 132 + class="size-6 md:size-9 text-base-50 dark:text-base-950" 133 + > 134 + <path 135 + fill-rule="evenodd" 136 + d="M6.75 5.25a.75.75 0 0 1 .75-.75H9a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H7.5a.75.75 0 0 1-.75-.75V5.25Zm7.5 0A.75.75 0 0 1 15 4.5h1.5a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V5.25Z" 137 + clip-rule="evenodd" 138 + /> 139 + </svg> 140 + {:else} 141 + <svg 142 + xmlns="http://www.w3.org/2000/svg" 143 + viewBox="0 0 24 24" 144 + fill="currentColor" 145 + class="ml-1 size-6 md:ml-1.5 md:size-9 text-base-50 dark:text-base-950" 146 + > 147 + <path 148 + fill-rule="evenodd" 149 + d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z" 150 + clip-rule="evenodd" 151 + /> 152 + </svg> 153 + {/if} 154 + <span class="sr-only">{isPlaying ? 'Pause' : 'Play'}</span> 155 + </button> 156 + <div bind:this={audioContainer} class="h-4 w-full grow"> 157 + {#if !isLoaded} 158 + <div class="m-1.5 h-1 w-full dark:bg-accent-950 bg-accent-100 rounded-full"></div> 159 + {/if} 160 + </div> 161 + {#if isLoaded} 162 + <NumberFlowGroup> 163 + <div 164 + class="text-accent-500 flex items-end justify-end font-semibold" 165 + style="font-variant-numeric: tabular-nums;" 166 + > 167 + -<NumberFlow 168 + value={remainingMinutes} 169 + trend={-1} 170 + format={{ minimumIntegerDigits: 2 }} 171 + digits={{ 1: { max: 5 } }} 172 + /> 173 + <NumberFlow 174 + value={remainingSeconds} 175 + trend={-1} 176 + format={{ minimumIntegerDigits: 2 }} 177 + digits={{ 1: { max: 5 } }} 178 + prefix=":" 179 + /> 180 + </div> 181 + </NumberFlowGroup> 182 + {:else} 183 + 184 + <div class="text-accent-500 flex items-end justify-end text-sm font-medium">loading</div> 185 + {/if} 186 + </div>
+5
src/routes/components/waveform-audio-player/+page.svelte
··· 1 + <script> 2 + import WaveformAudioPlayerPreview from '$lib/preview/WaveformAudioPlayerPreview.svelte'; 3 + </script> 4 + 5 + <WaveformAudioPlayerPreview />