replies timeline only, appview-less bluesky client
1<script lang="ts"> 2 import { generateColorForDid, type Account } from '$lib/accounts'; 3 import { AtpClient } from '$lib/at/client'; 4 import type { Did, Handle } from '@atcute/lexicons'; 5 import { theme } from '$lib/theme.svelte'; 6 7 let { 8 accounts = [], 9 selectedDid = $bindable(null), 10 onAccountSelected, 11 onLoginSucceed 12 }: { 13 accounts: Array<Account>; 14 selectedDid?: Did | null; 15 onAccountSelected: (did: Did) => void; 16 onLoginSucceed: (did: Did, handle: Handle, password: string) => void; 17 } = $props(); 18 19 let color = $derived(selectedDid ? (generateColorForDid(selectedDid) ?? theme.fg) : theme.fg); 20 21 let isDropdownOpen = $state(false); 22 let isLoginModalOpen = $state(false); 23 let loginHandle = $state(''); 24 let loginPassword = $state(''); 25 let loginError = $state(''); 26 let isLoggingIn = $state(false); 27 28 const toggleDropdown = (e: MouseEvent) => { 29 e.stopPropagation(); 30 isDropdownOpen = !isDropdownOpen; 31 }; 32 33 const selectAccount = (did: Did) => { 34 onAccountSelected(did); 35 isDropdownOpen = false; 36 }; 37 38 const openLoginModal = () => { 39 isLoginModalOpen = true; 40 isDropdownOpen = false; 41 loginHandle = ''; 42 loginPassword = ''; 43 loginError = ''; 44 }; 45 46 const closeLoginModal = () => { 47 isLoginModalOpen = false; 48 loginHandle = ''; 49 loginPassword = ''; 50 loginError = ''; 51 }; 52 53 const handleLogin = async () => { 54 if (!loginHandle || !loginPassword) { 55 loginError = 'please enter both handle and password'; 56 return; 57 } 58 59 isLoggingIn = true; 60 loginError = ''; 61 62 try { 63 const client = new AtpClient(); 64 const result = await client.login(loginHandle as Handle, loginPassword); 65 66 if (!result.ok) { 67 loginError = result.error; 68 isLoggingIn = false; 69 return; 70 } 71 72 if (!client.didDoc) { 73 loginError = 'failed to get did document'; 74 isLoggingIn = false; 75 return; 76 } 77 78 onLoginSucceed(client.didDoc.did, loginHandle as Handle, loginPassword); 79 closeLoginModal(); 80 } catch (error) { 81 loginError = `login failed: ${error}`; 82 } finally { 83 isLoggingIn = false; 84 } 85 }; 86 87 const handleKeydown = (event: KeyboardEvent) => { 88 if (event.key === 'Escape') { 89 closeLoginModal(); 90 } else if (event.key === 'Enter' && !isLoggingIn) { 91 handleLogin(); 92 } 93 }; 94 95 const closeDropdown = () => { 96 isDropdownOpen = false; 97 }; 98 99 let selectedAccount = $derived(accounts.find((acc) => acc.did === selectedDid)); 100</script> 101 102<svelte:window onclick={closeDropdown} /> 103 104<div class="relative"> 105 <button 106 onclick={toggleDropdown} 107 class="group flex h-full items-center gap-2 rounded-2xl border-2 px-4 font-medium shadow-lg transition-all hover:scale-105 hover:shadow-xl" 108 style="border-color: {theme.accent}66; background: {theme.accent}18; color: {color}; backdrop-filter: blur(8px);" 109 > 110 <span class="text-sm"> 111 {selectedAccount ? `@${selectedAccount.handle}` : 'select account'} 112 </span> 113 <svg 114 class="h-4 w-4 transition-transform {isDropdownOpen ? 'rotate-180' : ''}" 115 style="color: {theme.accent};" 116 fill="none" 117 stroke="currentColor" 118 viewBox="0 0 24 24" 119 > 120 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7" /> 121 </svg> 122 </button> 123 124 {#if isDropdownOpen} 125 <!-- svelte-ignore a11y_click_events_have_key_events --> 126 <!-- svelte-ignore a11y_no_static_element_interactions --> 127 <div 128 class="absolute left-0 z-10 mt-3 min-w-52 overflow-hidden rounded-2xl border-2 shadow-2xl backdrop-blur-lg" 129 style="border-color: {theme.accent}; background: {theme.bg}f0;" 130 onclick={(e) => e.stopPropagation()} 131 > 132 {#if accounts.length > 0} 133 <div class="p-2"> 134 {#each accounts as account (account.did)} 135 {@const color = generateColorForDid(account.did)} 136 <button 137 onclick={() => selectAccount(account.did)} 138 class="flex w-full items-center gap-3 rounded-xl p-2 text-left text-sm font-medium transition-all {account.did === 139 selectedDid 140 ? 'shadow-lg' 141 : 'hover:scale-[1.02]'}" 142 style="color: {color}; background: {account.did === selectedDid 143 ? `linear-gradient(135deg, ${theme.accent}33, ${theme.accent2}33)` 144 : 'transparent'};" 145 > 146 <span>@{account.handle}</span> 147 {#if account.did === selectedDid} 148 <svg 149 class="ml-auto h-5 w-5" 150 style="color: {theme.accent};" 151 fill="currentColor" 152 viewBox="0 0 20 20" 153 > 154 <path 155 fill-rule="evenodd" 156 d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" 157 clip-rule="evenodd" 158 /> 159 </svg> 160 {/if} 161 </button> 162 {/each} 163 </div> 164 <div 165 class="mx-2 h-px" 166 style="background: linear-gradient(to right, {theme.accent}, {theme.accent2});" 167 ></div> 168 {/if} 169 <button 170 onclick={openLoginModal} 171 class="flex w-full items-center gap-3 p-3 text-left text-sm font-semibold transition-all hover:scale-[1.02]" 172 style="color: {theme.accent};" 173 > 174 <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 175 <path 176 stroke-linecap="round" 177 stroke-linejoin="round" 178 stroke-width="2.5" 179 d="M12 4v16m8-8H4" 180 /> 181 </svg> 182 <span>add account</span> 183 </button> 184 </div> 185 {/if} 186</div> 187 188{#if isLoginModalOpen} 189 <div 190 class="fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm" 191 style="background: {theme.bg}cc;" 192 onclick={closeLoginModal} 193 onkeydown={handleKeydown} 194 role="button" 195 tabindex="-1" 196 > 197 <!-- svelte-ignore a11y_interactive_supports_focus --> 198 <!-- svelte-ignore a11y_click_events_have_key_events --> 199 <div 200 class="w-full max-w-md rounded-3xl border-2 p-5 shadow-2xl" 201 style="background: {theme.bg}; border-color: {theme.accent};" 202 onclick={(e) => e.stopPropagation()} 203 role="dialog" 204 > 205 <div class="mb-6 flex items-center justify-between"> 206 <div> 207 <h2 class="text-2xl font-bold" style="color: {theme.fg};">add account</h2> 208 <div class="mt-2 flex gap-2"> 209 <div class="h-1 w-10 rounded-full" style="background: {theme.accent};"></div> 210 <div class="h-1 w-9 rounded-full" style="background: {theme.accent2};"></div> 211 </div> 212 </div> 213 <!-- svelte-ignore a11y_consider_explicit_label --> 214 <button 215 onclick={closeLoginModal} 216 class="rounded-xl p-2 transition-all hover:scale-110" 217 style="color: {theme.fg}66; hover:color: {theme.fg};" 218 > 219 <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 220 <path 221 stroke-linecap="round" 222 stroke-linejoin="round" 223 stroke-width="2.5" 224 d="M6 18L18 6M6 6l12 12" 225 /> 226 </svg> 227 </button> 228 </div> 229 230 <div class="space-y-5"> 231 <div> 232 <label for="handle" class="mb-2 block text-sm font-semibold" style="color: {theme.fg}cc;"> 233 handle 234 </label> 235 <input 236 id="handle" 237 type="text" 238 bind:value={loginHandle} 239 placeholder="example.bsky.social" 240 class="placeholder-opacity-40 w-full rounded-xl border-2 px-4 py-3 font-medium transition-all focus:scale-[1.02] focus:shadow-lg focus:outline-none" 241 style="background: {theme.accent}08; border-color: {theme.accent}66; color: {theme.fg};" 242 disabled={isLoggingIn} 243 /> 244 </div> 245 246 <div> 247 <label 248 for="password" 249 class="mb-2 block text-sm font-semibold" 250 style="color: {theme.fg}cc;" 251 > 252 app password 253 </label> 254 <input 255 id="password" 256 type="password" 257 bind:value={loginPassword} 258 placeholder="xxxx-xxxx-xxxx-xxxx" 259 class="placeholder-opacity-40 w-full rounded-xl border-2 px-4 py-3 font-medium transition-all focus:scale-[1.02] focus:shadow-lg focus:outline-none" 260 style="background: {theme.accent}08; border-color: {theme.accent}66; color: {theme.fg};" 261 disabled={isLoggingIn} 262 /> 263 </div> 264 265 {#if loginError} 266 <div 267 class="rounded-xl border-2 p-4" 268 style="background: #ef444422; border-color: #ef4444;" 269 > 270 <p class="text-sm font-medium" style="color: #fca5a5;">{loginError}</p> 271 </div> 272 {/if} 273 274 <div class="flex gap-3 pt-3"> 275 <button 276 onclick={closeLoginModal} 277 class="flex-1 rounded-xl border-2 px-5 py-3 font-semibold transition-all hover:scale-105" 278 style="background: {theme.bg}; border-color: {theme.fg}33; color: {theme.fg};" 279 disabled={isLoggingIn} 280 > 281 cancel 282 </button> 283 <button 284 onclick={handleLogin} 285 class="flex-1 rounded-xl border-2 px-5 py-3 font-semibold transition-all hover:scale-105 hover:shadow-xl disabled:cursor-not-allowed disabled:opacity-50" 286 style="background: linear-gradient(135deg, {theme.accent}, {theme.accent2}); border-color: transparent; color: {theme.fg};" 287 disabled={isLoggingIn} 288 > 289 {isLoggingIn ? 'logging in...' : 'login'} 290 </button> 291 </div> 292 </div> 293 </div> 294 </div> 295{/if}