at main 5.0 kB view raw
1<script lang="ts"> 2 import type { AlbumSummary } from '$lib/types'; 3 4 interface Props { 5 albums: AlbumSummary[]; 6 value: string; 7 placeholder?: string; 8 disabled?: boolean; 9 } 10 11 let { albums = [], value = $bindable(''), placeholder = 'album name', disabled = false }: Props = $props(); 12 13 let showResults = $state(false); 14 let filteredAlbums = $derived.by(() => { 15 if (!value || value.length === 0) { 16 return albums; 17 } 18 return albums.filter(album => 19 album.title.toLowerCase().includes(value.toLowerCase()) 20 ); 21 }); 22 23 let exactMatch = $derived.by(() => { 24 return albums.find(a => a.title.toLowerCase() === value.toLowerCase()); 25 }); 26 27 let similarAlbums = $derived.by(() => { 28 if (exactMatch || !value) return []; 29 return albums.filter(a => 30 a.title.toLowerCase() !== value.toLowerCase() && 31 a.title.toLowerCase().includes(value.toLowerCase()) 32 ); 33 }); 34 35 function selectAlbum(albumTitle: string) { 36 value = albumTitle; 37 showResults = false; 38 } 39 40 function handleClickOutside(e: MouseEvent) { 41 const target = e.target as HTMLElement; 42 if (!target.closest('.album-select-container')) { 43 showResults = false; 44 } 45 } 46</script> 47 48<svelte:window onclick={handleClickOutside} /> 49 50<div class="album-select-container"> 51 <div class="input-wrapper"> 52 <input 53 type="text" 54 bind:value 55 placeholder={placeholder} 56 {disabled} 57 class="album-input" 58 onfocus={() => { if (albums.length > 0) showResults = true; }} 59 oninput={() => { showResults = albums.length > 0; }} 60 autocomplete="off" 61 /> 62 63 {#if showResults && filteredAlbums.length > 0} 64 <div class="album-results"> 65 {#each filteredAlbums as album} 66 <button 67 type="button" 68 class="album-result-item" 69 class:exact-match={album.title.toLowerCase() === value.toLowerCase()} 70 onclick={() => selectAlbum(album.title)} 71 > 72 <div class="album-info"> 73 <div class="album-title">{album.title}</div> 74 <div class="album-stats"> 75 {album.track_count} {album.track_count === 1 ? 'track' : 'tracks'} 76 </div> 77 </div> 78 </button> 79 {/each} 80 </div> 81 {/if} 82 </div> 83 84 {#if !exactMatch && similarAlbums.length > 0} 85 <p class="similar-hint"> 86 similar: {similarAlbums.map(a => a.title).join(', ')} 87 </p> 88 {/if} 89</div> 90 91<style> 92 .album-select-container { 93 width: 100%; 94 } 95 96 .input-wrapper { 97 position: relative; 98 } 99 100 .album-input { 101 width: 100%; 102 padding: 0.75rem; 103 background: var(--bg-primary); 104 border: 1px solid var(--border-default); 105 border-radius: var(--radius-sm); 106 color: var(--text-primary); 107 font-size: var(--text-lg); 108 font-family: inherit; 109 transition: all 0.2s; 110 } 111 112 .album-input:focus { 113 outline: none; 114 border-color: var(--accent); 115 } 116 117 .album-input:disabled { 118 opacity: 0.5; 119 cursor: not-allowed; 120 } 121 122 .album-results { 123 position: absolute; 124 z-index: 100; 125 width: 100%; 126 max-height: 300px; 127 overflow-y: auto; 128 background: var(--bg-tertiary); 129 border: 1px solid var(--border-default); 130 border-radius: var(--radius-sm); 131 margin-top: 0.25rem; 132 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); 133 } 134 135 /* custom scrollbar styling */ 136 .album-results::-webkit-scrollbar { 137 width: 8px; 138 } 139 140 .album-results::-webkit-scrollbar-track { 141 background: var(--bg-primary); 142 border-radius: var(--radius-sm); 143 } 144 145 .album-results::-webkit-scrollbar-thumb { 146 background: var(--border-default); 147 border-radius: var(--radius-sm); 148 } 149 150 .album-results::-webkit-scrollbar-thumb:hover { 151 background: var(--border-emphasis); 152 } 153 154 /* firefox scrollbar */ 155 .album-results { 156 scrollbar-width: thin; 157 scrollbar-color: var(--border-default) var(--bg-primary); 158 } 159 160 .album-result-item { 161 width: 100%; 162 display: flex; 163 align-items: center; 164 gap: 0.75rem; 165 padding: 0.75rem; 166 background: transparent; 167 border: none; 168 border-bottom: 1px solid var(--border-subtle); 169 color: var(--text-primary); 170 text-align: left; 171 font-family: inherit; 172 cursor: pointer; 173 transition: all 0.15s; 174 min-width: 0; 175 } 176 177 .album-result-item:last-child { 178 border-bottom: none; 179 } 180 181 .album-result-item:hover { 182 background: var(--bg-hover); 183 } 184 185 .album-result-item.exact-match { 186 background: color-mix(in srgb, var(--accent) 10%, transparent); 187 border-left: 3px solid var(--accent); 188 } 189 190 .album-info { 191 flex: 1; 192 min-width: 0; 193 overflow: hidden; 194 } 195 196 .album-title { 197 font-weight: 500; 198 color: var(--text-primary); 199 margin-bottom: 0.125rem; 200 overflow: hidden; 201 text-overflow: ellipsis; 202 white-space: nowrap; 203 } 204 205 .album-stats { 206 font-size: var(--text-sm); 207 color: var(--text-tertiary); 208 overflow: hidden; 209 text-overflow: ellipsis; 210 white-space: nowrap; 211 } 212 213 .similar-hint { 214 margin-top: 0.5rem; 215 font-size: var(--text-sm); 216 color: var(--warning); 217 font-style: italic; 218 margin-bottom: 0; 219 } 220 221 /* mobile styles */ 222 @media (max-width: 768px) { 223 .album-input { 224 font-size: 16px; /* prevents zoom on iOS */ 225 } 226 227 .album-results { 228 max-height: 200px; 229 } 230 231 .album-result-item { 232 padding: 0.625rem; 233 } 234 } 235</style>