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}