@mrgnw/anahtar 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +17 -2
  2. package/dist/components/AuthPill.svelte +650 -0
  3. package/dist/components/AuthPill.svelte.d.ts +22 -0
  4. package/dist/components/PasskeyPrompt.svelte +21 -3
  5. package/dist/components/index.d.ts +7 -6
  6. package/dist/components/index.js +6 -5
  7. package/dist/i18n/af.js +1 -1
  8. package/dist/i18n/am.js +1 -1
  9. package/dist/i18n/ar.js +1 -1
  10. package/dist/i18n/as.js +1 -1
  11. package/dist/i18n/az.js +1 -1
  12. package/dist/i18n/be.js +1 -1
  13. package/dist/i18n/bg.js +1 -1
  14. package/dist/i18n/bn.js +1 -1
  15. package/dist/i18n/ca.js +1 -1
  16. package/dist/i18n/el.js +1 -1
  17. package/dist/i18n/es.js +1 -1
  18. package/dist/i18n/fa.js +1 -1
  19. package/dist/i18n/fil.js +1 -1
  20. package/dist/i18n/fr.js +1 -1
  21. package/dist/i18n/gu.js +1 -1
  22. package/dist/i18n/ha.js +1 -1
  23. package/dist/i18n/he.js +1 -1
  24. package/dist/i18n/hi.js +1 -1
  25. package/dist/i18n/ht.js +1 -1
  26. package/dist/i18n/hy.js +1 -1
  27. package/dist/i18n/id.js +1 -1
  28. package/dist/i18n/ig.js +1 -1
  29. package/dist/i18n/index.js +99 -43
  30. package/dist/i18n/it.js +1 -1
  31. package/dist/i18n/ja.js +1 -1
  32. package/dist/i18n/jv.js +1 -1
  33. package/dist/i18n/kk.js +1 -1
  34. package/dist/i18n/km.js +1 -1
  35. package/dist/i18n/kn.js +1 -1
  36. package/dist/i18n/ko.js +1 -1
  37. package/dist/i18n/lo.js +1 -1
  38. package/dist/i18n/mg.js +1 -1
  39. package/dist/i18n/ml.js +1 -1
  40. package/dist/i18n/mn.js +1 -1
  41. package/dist/i18n/mr.js +1 -1
  42. package/dist/i18n/ms.js +1 -1
  43. package/dist/i18n/my.js +1 -1
  44. package/dist/i18n/ne.js +1 -1
  45. package/dist/i18n/or.js +1 -1
  46. package/dist/i18n/pa.js +1 -1
  47. package/dist/i18n/ps.js +1 -1
  48. package/dist/i18n/pt.js +1 -1
  49. package/dist/i18n/ro.js +1 -1
  50. package/dist/i18n/ru.js +1 -1
  51. package/dist/i18n/rw.js +1 -1
  52. package/dist/i18n/sd.js +1 -1
  53. package/dist/i18n/si.js +1 -1
  54. package/dist/i18n/so.js +1 -1
  55. package/dist/i18n/sq.js +1 -1
  56. package/dist/i18n/sr.js +1 -1
  57. package/dist/i18n/st.js +1 -1
  58. package/dist/i18n/su.js +1 -1
  59. package/dist/i18n/sw.js +1 -1
  60. package/dist/i18n/ta.js +1 -1
  61. package/dist/i18n/te.js +1 -1
  62. package/dist/i18n/tg.js +1 -1
  63. package/dist/i18n/th.js +1 -1
  64. package/dist/i18n/ti.js +1 -1
  65. package/dist/i18n/tk.js +1 -1
  66. package/dist/i18n/tr.js +1 -1
  67. package/dist/i18n/ts.js +1 -1
  68. package/dist/i18n/tt.js +1 -1
  69. package/dist/i18n/ug.js +1 -1
  70. package/dist/i18n/uk.js +1 -1
  71. package/dist/i18n/uz.js +1 -1
  72. package/dist/i18n/vi.js +1 -1
  73. package/dist/i18n/yo.js +1 -1
  74. package/dist/i18n/zh.js +1 -1
  75. package/dist/i18n/zu.js +1 -1
  76. package/dist/kit/handlers.js +16 -0
  77. package/package.json +9 -15
package/README.md CHANGED
@@ -51,9 +51,10 @@ import { auth } from "$lib/server/auth";
51
51
  export const { GET, POST } = auth.handlers;
52
52
  ```
53
53
 
54
- Optional UI component:
54
+ Optional UI components:
55
55
 
56
56
  ```svelte
57
+ <!-- Full-page auth flow -->
57
58
  <script>
58
59
  import { AuthFlow } from '@mrgnw/anahtar/components';
59
60
  import { goto } from '$app/navigation';
@@ -62,6 +63,20 @@ Optional UI component:
62
63
  <AuthFlow onSuccess={() => goto('/')} />
63
64
  ```
64
65
 
66
+ ```svelte
67
+ <!-- Compact inline pill (for headers, floating islands) -->
68
+ <script>
69
+ import { AuthPill } from '@mrgnw/anahtar/components';
70
+ import { invalidateAll } from '$app/navigation';
71
+ import { page } from '$app/stores';
72
+ let user = $derived($page.data.user);
73
+ </script>
74
+
75
+ <AuthPill {user} onSuccess={() => invalidateAll()} />
76
+ ```
77
+
78
+ All components auto-detect locale (88 languages). Override with `locale="fr"` or `messages={{ continue: 'Go' }}`.
79
+
65
80
  ## Tests
66
81
 
67
82
  68 tests: 46 unit (node) + 22 component (happy-dom).
@@ -74,5 +89,5 @@ pnpm test
74
89
 
75
90
  ## Docs
76
91
 
77
- - [Integration guide](docs/integration.md) — install, config, theming, Postgres setup
92
+ - [Integration guide](docs/integration.md) — install, config, components, i18n, theming, DB adapters
78
93
  - [PLAN.md](PLAN.md) — architecture, DB adapter interface, test setup
@@ -0,0 +1,650 @@
1
+ <script lang="ts">
2
+ import { guessDeviceName } from '../device.js';
3
+ import { resolveMessages, detectLocaleClient, type AuthMessages } from '../i18n/index.js';
4
+ import PasskeyPrompt from './PasskeyPrompt.svelte';
5
+ import { onMount } from 'svelte';
6
+ import { slide } from 'svelte/transition';
7
+
8
+ interface PasskeyInfo {
9
+ id: string;
10
+ credentialId?: string;
11
+ name?: string | null;
12
+ createdAt?: number;
13
+ }
14
+
15
+ interface Props {
16
+ apiBase?: string;
17
+ user?: { email: string } | null;
18
+ locale?: string;
19
+ messages?: Partial<AuthMessages>;
20
+ onSuccess?: () => void | Promise<void>;
21
+ onSignOut?: () => void | Promise<void>;
22
+ onPasskeysChange?: () => void | Promise<void>;
23
+ getPasskeys?: () => Promise<PasskeyInfo[]>;
24
+ }
25
+
26
+ let {
27
+ apiBase = '/api/auth',
28
+ user = null,
29
+ locale,
30
+ messages: messageOverrides,
31
+ onSuccess,
32
+ onSignOut,
33
+ onPasskeysChange,
34
+ getPasskeys,
35
+ }: Props = $props();
36
+
37
+ let m = $derived(resolveMessages(locale ?? detectLocaleClient(), messageOverrides));
38
+
39
+ let email = $state('');
40
+ let loading = $state(false);
41
+ let error = $state('');
42
+ let otpStep = $state(false);
43
+ let otpDigits = $state<string[]>(['', '', '', '', '']);
44
+ let otpInputs = $state<HTMLInputElement[]>([]);
45
+ let showPasskeys = $state(false);
46
+ let hoveredKey = $state<string | null>(null);
47
+ let passkeyRefresh = $state(0);
48
+ let passkeyOnboarding = $state(false);
49
+ let conditionalAbort: AbortController | null = null;
50
+
51
+ const isAuthenticated = $derived(!!user);
52
+ const passkeyPromise = $derived(
53
+ isAuthenticated && getPasskeys && passkeyRefresh >= 0 ? getPasskeys() : null
54
+ );
55
+
56
+ onMount(() => {
57
+ if (!isAuthenticated) tryConditionalWebAuthn();
58
+ return () => conditionalAbort?.abort();
59
+ });
60
+
61
+ async function handleSignOut() {
62
+ email = '';
63
+ otpStep = false;
64
+ passkeyOnboarding = false;
65
+ showPasskeys = false;
66
+ error = '';
67
+ await onSignOut?.();
68
+ }
69
+
70
+ async function tryConditionalWebAuthn() {
71
+ try {
72
+ const { startAuthentication } = await import('@simplewebauthn/browser');
73
+ const res = await fetch(`${apiBase}/passkey/login-start`);
74
+ if (!res.ok) return;
75
+ const options = (await res.json()) as any;
76
+ conditionalAbort = new AbortController();
77
+ const authResponse = await startAuthentication({
78
+ optionsJSON: options,
79
+ useBrowserAutofill: true,
80
+ });
81
+ loading = true;
82
+ const verifyRes = await fetch(`${apiBase}/passkey/login-finish`, {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify(authResponse),
86
+ });
87
+ if (verifyRes.ok) await onSuccess?.();
88
+ } catch {
89
+ /* not available or cancelled */
90
+ } finally {
91
+ loading = false;
92
+ }
93
+ }
94
+
95
+ async function handleEmailSubmit(e: SubmitEvent) {
96
+ e.preventDefault();
97
+ if (!email.includes('@')) return;
98
+ conditionalAbort?.abort();
99
+ conditionalAbort = null;
100
+ loading = true;
101
+ error = '';
102
+ try {
103
+ // Passkey-first: check if user has passkeys before falling back to OTP
104
+ try {
105
+ const { startAuthentication } = await import('@simplewebauthn/browser');
106
+ const checkRes = await fetch(`${apiBase}/passkey/check-email`, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ email }),
110
+ });
111
+ if (checkRes.ok) {
112
+ const opts = (await checkRes.json()) as any;
113
+ if ((opts?.allowCredentials?.length ?? 0) > 0) {
114
+ try {
115
+ const authResp = await startAuthentication({ optionsJSON: opts });
116
+ const vRes = await fetch(`${apiBase}/passkey/login-finish`, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify(authResp),
120
+ });
121
+ if (vRes.ok) {
122
+ await onSuccess?.();
123
+ return;
124
+ }
125
+ } catch {
126
+ /* cancelled */
127
+ }
128
+ }
129
+ }
130
+ } catch {
131
+ /* passkey check failed */
132
+ }
133
+
134
+ const res = await fetch(`${apiBase}/start`, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ email }),
138
+ });
139
+ if (!res.ok) {
140
+ const d = ((await res.json().catch(() => ({}))) as any);
141
+ error = d?.error ?? m.errorGeneric;
142
+ return;
143
+ }
144
+ const data = ((await res.json().catch(() => ({}))) as any);
145
+ otpDigits = ['', '', '', '', ''];
146
+ otpStep = true;
147
+ if (data?.devCode) {
148
+ const code = String(data.devCode);
149
+ for (let i = 0; i < 5; i++) otpDigits[i] = code[i] ?? '';
150
+ setTimeout(() => verifyOtp(code), 50);
151
+ } else {
152
+ setTimeout(() => otpInputs[0]?.focus(), 50);
153
+ }
154
+ } catch {
155
+ error = m.errorGeneric;
156
+ } finally {
157
+ loading = false;
158
+ }
159
+ }
160
+
161
+ function handleOtpInput(i: number, e: Event) {
162
+ const input = e.target as HTMLInputElement;
163
+ const val = input.value.replace(/\D/g, '');
164
+ if (val.length > 1) {
165
+ for (let j = 0; j < 5; j++) otpDigits[j] = val[j] ?? '';
166
+ const next = otpDigits.findIndex((d) => !d);
167
+ otpInputs[next === -1 ? 4 : next]?.focus();
168
+ } else {
169
+ otpDigits[i] = val.slice(0, 1);
170
+ input.value = otpDigits[i];
171
+ if (val && i < 4) otpInputs[i + 1]?.focus();
172
+ }
173
+ if (otpDigits.every((d) => d.length === 1)) verifyOtp(otpDigits.join(''));
174
+ }
175
+
176
+ function handleOtpKeydown(i: number, e: KeyboardEvent) {
177
+ if (e.key === 'Backspace' && !otpDigits[i] && i > 0) otpInputs[i - 1]?.focus();
178
+ }
179
+
180
+ function handleOtpPaste(e: ClipboardEvent) {
181
+ e.preventDefault();
182
+ const pasted = (e.clipboardData?.getData('text') ?? '').replace(/\D/g, '');
183
+ for (let i = 0; i < 5; i++) otpDigits[i] = pasted[i] ?? '';
184
+ const next = otpDigits.findIndex((d) => !d);
185
+ otpInputs[next === -1 ? 4 : next]?.focus();
186
+ if (otpDigits.every((d) => d.length === 1)) verifyOtp(otpDigits.join(''));
187
+ }
188
+
189
+ async function verifyOtp(code: string) {
190
+ loading = true;
191
+ error = '';
192
+ try {
193
+ const res = await fetch(`${apiBase}/verify`, {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({ email, code }),
197
+ });
198
+ if (!res.ok) {
199
+ const d = ((await res.json().catch(() => ({}))) as any);
200
+ error = d?.error ?? m.errorInvalidCode;
201
+ otpDigits = ['', '', '', '', ''];
202
+ setTimeout(() => otpInputs[0]?.focus(), 50);
203
+ return;
204
+ }
205
+ const data = ((await res.json().catch(() => ({}))) as any);
206
+ await onSuccess?.();
207
+ otpStep = false;
208
+ if (!data.hasPasskey && !data.skipPasskeyPrompt) {
209
+ passkeyOnboarding = true;
210
+ }
211
+ } catch {
212
+ error = m.errorGeneric;
213
+ } finally {
214
+ loading = false;
215
+ }
216
+ }
217
+
218
+ async function resend() {
219
+ loading = true;
220
+ try {
221
+ await fetch(`${apiBase}/start`, {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify({ email }),
225
+ });
226
+ otpDigits = ['', '', '', '', ''];
227
+ setTimeout(() => otpInputs[0]?.focus(), 50);
228
+ } catch {
229
+ /* ignore */
230
+ } finally {
231
+ loading = false;
232
+ }
233
+ }
234
+
235
+ async function handlePasskeyRegister() {
236
+ const { startRegistration } = await import('@simplewebauthn/browser');
237
+ const optRes = await fetch(`${apiBase}/passkey/register-start`, { method: 'POST' });
238
+ if (!optRes.ok) throw new Error('Failed to get registration options');
239
+ const options = (await optRes.json()) as any;
240
+ const regResponse = await startRegistration({ optionsJSON: options });
241
+ const res = await fetch(`${apiBase}/passkey/register-finish`, {
242
+ method: 'POST',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({ ...regResponse, name: guessDeviceName() }),
245
+ });
246
+ if (!res.ok) throw new Error('Registration failed');
247
+ passkeyOnboarding = false;
248
+ passkeyRefresh++;
249
+ await onPasskeysChange?.();
250
+ }
251
+
252
+ function handlePasskeySkip() {
253
+ fetch(`${apiBase}/skip-passkey`, { method: 'POST' });
254
+ passkeyOnboarding = false;
255
+ }
256
+
257
+ async function addPasskey() {
258
+ loading = true;
259
+ error = '';
260
+ try {
261
+ const { startRegistration } = await import('@simplewebauthn/browser');
262
+ const startRes = await fetch(`${apiBase}/passkey/register-start`, { method: 'POST' });
263
+ if (!startRes.ok) {
264
+ error = 'Failed to start';
265
+ return;
266
+ }
267
+ const opts = (await startRes.json()) as any;
268
+ const regResponse = await startRegistration({ optionsJSON: opts });
269
+ const finishRes = await fetch(`${apiBase}/passkey/register-finish`, {
270
+ method: 'POST',
271
+ headers: { 'Content-Type': 'application/json' },
272
+ body: JSON.stringify({ ...regResponse, name: guessDeviceName() }),
273
+ });
274
+ if (!finishRes.ok) {
275
+ error = 'Failed to register';
276
+ return;
277
+ }
278
+ passkeyRefresh++;
279
+ await onPasskeysChange?.();
280
+ } catch (e) {
281
+ error = e instanceof Error ? e.message : 'Failed';
282
+ } finally {
283
+ loading = false;
284
+ }
285
+ }
286
+
287
+ async function removePasskey(id: string) {
288
+ loading = true;
289
+ try {
290
+ await fetch(`${apiBase}/passkey/remove`, {
291
+ method: 'POST',
292
+ headers: { 'Content-Type': 'application/json' },
293
+ body: JSON.stringify({ passkeyId: id }),
294
+ });
295
+ passkeyRefresh++;
296
+ await onPasskeysChange?.();
297
+ } catch {
298
+ /* ignore */
299
+ } finally {
300
+ loading = false;
301
+ }
302
+ }
303
+ </script>
304
+
305
+ <div class="anahtar-pill-island" class:anahtar-pill-loading={loading}>
306
+ <div class="anahtar-pill">
307
+ {#if isAuthenticated}
308
+ <span class="anahtar-pill-email">{user?.email}</span>
309
+ <span class="anahtar-pill-sep">&middot;</span>
310
+ {#if getPasskeys}
311
+ <button
312
+ class="anahtar-pill-icon"
313
+ class:anahtar-pill-icon-active={showPasskeys}
314
+ onclick={() => (showPasskeys = !showPasskeys)}
315
+ title="Passkeys"
316
+ disabled={loading}
317
+ >
318
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
319
+ <circle cx="7.5" cy="15.5" r="5.5"/><path d="m11.5 12 4-4"/><path d="m15 7 2 2"/><path d="m17.5 4.5 2 2"/>
320
+ </svg>
321
+ </button>
322
+ <span class="anahtar-pill-sep">&middot;</span>
323
+ {/if}
324
+ <button class="anahtar-pill-icon anahtar-pill-signout" onclick={handleSignOut} title="Sign out" disabled={loading}>
325
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
326
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>
327
+ </svg>
328
+ </button>
329
+
330
+ {:else if otpStep}
331
+ <span class="anahtar-pill-otp-label">{email}</span>
332
+ <span class="anahtar-pill-sep">&middot;</span>
333
+ <div class="anahtar-pill-otp-boxes">
334
+ {#each otpDigits as _, i}
335
+ <input
336
+ bind:this={otpInputs[i]}
337
+ class="anahtar-pill-otp-box"
338
+ type="text"
339
+ inputmode="numeric"
340
+ autocomplete={i === 0 ? 'one-time-code' : 'off'}
341
+ value={otpDigits[i]}
342
+ disabled={loading}
343
+ oninput={(e) => handleOtpInput(i, e)}
344
+ onkeydown={(e) => handleOtpKeydown(i, e)}
345
+ onpaste={handleOtpPaste}
346
+ />
347
+ {/each}
348
+ </div>
349
+
350
+ {:else}
351
+ <form onsubmit={handleEmailSubmit} class="anahtar-pill-form">
352
+ <input
353
+ type="email"
354
+ bind:value={email}
355
+ placeholder={m.emailPlaceholder}
356
+ class="anahtar-pill-email-input"
357
+ autocomplete="username webauthn"
358
+ disabled={loading}
359
+ />
360
+ <button type="submit" class="anahtar-pill-go" disabled={loading || !email.includes('@')}>
361
+ {loading ? '...' : m.continue}
362
+ </button>
363
+ </form>
364
+ {/if}
365
+ </div>
366
+
367
+ {#if otpStep}
368
+ <div class="anahtar-pill-otp-help" transition:slide={{ duration: 150 }}>
369
+ <span class="anahtar-pill-otp-help-text">{m.codeSentTo}</span>
370
+ <span class="anahtar-pill-otp-help-sep">&middot;</span>
371
+ <button class="anahtar-pill-otp-help-link" onclick={() => { otpStep = false; error = ''; }}>{m.differentEmail}</button>
372
+ <span class="anahtar-pill-otp-help-sep">&middot;</span>
373
+ <button class="anahtar-pill-otp-help-link" onclick={resend} disabled={loading}>{m.resend}</button>
374
+ </div>
375
+ {/if}
376
+
377
+ {#if error}
378
+ <p class="anahtar-pill-error" transition:slide={{ duration: 150 }}>{error}</p>
379
+ {/if}
380
+
381
+ {#if passkeyOnboarding && isAuthenticated}
382
+ <div class="anahtar-pill-onboarding" transition:slide={{ duration: 200 }}>
383
+ <PasskeyPrompt {m} onRegister={handlePasskeyRegister} onSkip={handlePasskeySkip} />
384
+ </div>
385
+ {/if}
386
+
387
+ {#if showPasskeys && isAuthenticated && !passkeyOnboarding}
388
+ <div class="anahtar-pill-passkeys" transition:slide={{ duration: 180 }}>
389
+ {#if passkeyPromise}
390
+ {#await passkeyPromise then keys}
391
+ {#each keys as key}
392
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
393
+ <div
394
+ class="anahtar-pill-key-row"
395
+ onmouseenter={() => (hoveredKey = key.id)}
396
+ onmouseleave={() => (hoveredKey = null)}
397
+ >
398
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
399
+ <circle cx="7.5" cy="15.5" r="5.5"/><path d="m11.5 12 4-4"/><path d="m15 7 2 2"/><path d="m17.5 4.5 2 2"/>
400
+ </svg>
401
+ <span class="anahtar-pill-key-name">{key.name ?? 'Passkey'}</span>
402
+ {#if hoveredKey === key.id}
403
+ <button
404
+ class="anahtar-pill-key-remove"
405
+ onclick={() => removePasskey(key.id)}
406
+ disabled={loading}
407
+ >&times;</button>
408
+ {/if}
409
+ </div>
410
+ {/each}
411
+ {/await}
412
+ {/if}
413
+ <button class="anahtar-pill-key-add" onclick={addPasskey} disabled={loading}>
414
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
415
+ <line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/>
416
+ </svg>
417
+ Add passkey
418
+ </button>
419
+ </div>
420
+ {/if}
421
+ </div>
422
+
423
+ <style>
424
+ .anahtar-pill-island {
425
+ display: flex;
426
+ flex-direction: column;
427
+ align-items: flex-end;
428
+ gap: 0.25rem;
429
+ }
430
+
431
+ .anahtar-pill-loading { opacity: 0.7; }
432
+
433
+ .anahtar-pill {
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 0.4rem;
437
+ background: var(--anahtar-pill-bg, rgba(255, 255, 255, 0.9));
438
+ backdrop-filter: blur(8px);
439
+ border: 1px solid var(--anahtar-pill-border, rgba(0, 0, 0, 0.06));
440
+ border-radius: 9999px;
441
+ padding: 0.25rem 0.75rem;
442
+ box-shadow: var(--anahtar-pill-shadow, 0 2px 12px rgba(0, 0, 0, 0.08));
443
+ font-size: 0.875rem;
444
+ white-space: nowrap;
445
+ height: 2.25rem;
446
+ box-sizing: border-box;
447
+ }
448
+
449
+ .anahtar-pill-sep {
450
+ color: var(--anahtar-pill-sep, rgba(0, 0, 0, 0.2));
451
+ font-size: 0.75rem;
452
+ user-select: none;
453
+ }
454
+
455
+ .anahtar-pill-email {
456
+ font-size: 0.8125rem;
457
+ font-weight: 500;
458
+ color: var(--anahtar-pill-fg, #374151);
459
+ max-width: 200px;
460
+ overflow: hidden;
461
+ text-overflow: ellipsis;
462
+ }
463
+
464
+ .anahtar-pill-icon {
465
+ display: flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ background: none;
469
+ border: none;
470
+ cursor: pointer;
471
+ padding: 0.2rem;
472
+ border-radius: 9999px;
473
+ color: var(--anahtar-pill-icon, #6b7280);
474
+ transition: color 0.15s, background 0.15s;
475
+ line-height: 1;
476
+ }
477
+ .anahtar-pill-icon:hover:not(:disabled) { color: var(--anahtar-pill-fg, #111827); background: rgba(0,0,0,0.06); }
478
+ .anahtar-pill-icon:disabled { opacity: 0.4; cursor: not-allowed; }
479
+ .anahtar-pill-icon-active { color: var(--anahtar-primary, #3730a3); }
480
+ .anahtar-pill-signout:hover:not(:disabled) { color: var(--anahtar-error, #ef4444); }
481
+
482
+ /* Sign-in form */
483
+ .anahtar-pill-form {
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 0.375rem;
487
+ }
488
+
489
+ .anahtar-pill-email-input {
490
+ border: none;
491
+ background: transparent;
492
+ outline: none;
493
+ font-size: 0.875rem;
494
+ color: var(--anahtar-pill-fg, #111827);
495
+ width: 190px;
496
+ padding: 0.125rem 0;
497
+ }
498
+ .anahtar-pill-email-input::placeholder { color: var(--anahtar-pill-placeholder, #9ca3af); }
499
+
500
+ .anahtar-pill-go {
501
+ background: var(--anahtar-primary, #3730a3);
502
+ color: var(--anahtar-primary-fg, #fff);
503
+ border: none;
504
+ border-radius: 9999px;
505
+ padding: 0.25rem 0.75rem;
506
+ font-size: 0.8125rem;
507
+ font-weight: 500;
508
+ cursor: pointer;
509
+ transition: opacity 0.15s;
510
+ white-space: nowrap;
511
+ }
512
+ .anahtar-pill-go:disabled { opacity: 0.4; cursor: not-allowed; }
513
+ .anahtar-pill-go:hover:not(:disabled) { opacity: 0.85; }
514
+
515
+ /* OTP */
516
+ .anahtar-pill-otp-label {
517
+ font-size: 0.8125rem;
518
+ color: var(--anahtar-pill-fg, #374151);
519
+ font-weight: 500;
520
+ max-width: 150px;
521
+ overflow: hidden;
522
+ text-overflow: ellipsis;
523
+ }
524
+
525
+ .anahtar-pill-otp-boxes { display: flex; gap: 0.2rem; }
526
+
527
+ .anahtar-pill-otp-box {
528
+ width: 1.75rem;
529
+ height: 1.75rem;
530
+ text-align: center;
531
+ font-size: 0.9375rem;
532
+ border: 1px solid var(--anahtar-pill-border, rgba(0,0,0,0.15));
533
+ border-radius: 0.3rem;
534
+ background: var(--anahtar-pill-bg, rgba(255,255,255,0.8));
535
+ outline: none;
536
+ color: var(--anahtar-pill-fg, #111827);
537
+ }
538
+ .anahtar-pill-otp-box:focus {
539
+ border-color: var(--anahtar-primary, #3730a3);
540
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--anahtar-primary, #3730a3) 20%, transparent);
541
+ }
542
+
543
+ /* OTP helper row */
544
+ .anahtar-pill-otp-help {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 0.375rem;
548
+ padding: 0 0.5rem;
549
+ }
550
+
551
+ .anahtar-pill-otp-help-text {
552
+ font-size: 0.75rem;
553
+ color: var(--anahtar-pill-icon, #6b7280);
554
+ }
555
+
556
+ .anahtar-pill-otp-help-sep {
557
+ color: var(--anahtar-pill-sep, rgba(0, 0, 0, 0.15));
558
+ font-size: 0.625rem;
559
+ user-select: none;
560
+ }
561
+
562
+ .anahtar-pill-otp-help-link {
563
+ font-size: 0.75rem;
564
+ color: var(--anahtar-primary, #3730a3);
565
+ background: none;
566
+ border: none;
567
+ cursor: pointer;
568
+ padding: 0;
569
+ transition: opacity 0.15s;
570
+ }
571
+ .anahtar-pill-otp-help-link:hover:not(:disabled) { opacity: 0.7; }
572
+ .anahtar-pill-otp-help-link:disabled { opacity: 0.4; cursor: not-allowed; }
573
+
574
+ /* Passkey onboarding card */
575
+ .anahtar-pill-onboarding {
576
+ background: var(--anahtar-pill-bg, rgba(255,255,255,0.97));
577
+ backdrop-filter: blur(12px);
578
+ border: 1px solid var(--anahtar-pill-border, rgba(0,0,0,0.06));
579
+ border-radius: 1rem;
580
+ padding: 1.25rem 1.5rem;
581
+ box-shadow: var(--anahtar-pill-shadow, 0 4px 24px rgba(0,0,0,0.1));
582
+ min-width: 220px;
583
+ }
584
+
585
+ /* Passkey panel */
586
+ .anahtar-pill-passkeys {
587
+ background: var(--anahtar-pill-bg, rgba(255,255,255,0.95));
588
+ backdrop-filter: blur(8px);
589
+ border: 1px solid var(--anahtar-pill-border, rgba(0,0,0,0.06));
590
+ border-radius: 0.75rem;
591
+ padding: 0.5rem 0.75rem;
592
+ box-shadow: var(--anahtar-pill-shadow, 0 2px 12px rgba(0,0,0,0.08));
593
+ display: flex;
594
+ flex-direction: column;
595
+ gap: 0.25rem;
596
+ min-width: 180px;
597
+ }
598
+
599
+ .anahtar-pill-key-row {
600
+ display: flex;
601
+ align-items: center;
602
+ gap: 0.375rem;
603
+ font-size: 0.8125rem;
604
+ color: var(--anahtar-pill-fg, #374151);
605
+ padding: 0.2rem 0;
606
+ }
607
+
608
+ .anahtar-pill-key-name {
609
+ flex: 1;
610
+ overflow: hidden;
611
+ text-overflow: ellipsis;
612
+ white-space: nowrap;
613
+ }
614
+
615
+ .anahtar-pill-key-remove {
616
+ background: none;
617
+ border: none;
618
+ color: var(--anahtar-pill-icon, #9ca3af);
619
+ cursor: pointer;
620
+ font-size: 1rem;
621
+ line-height: 1;
622
+ padding: 0 0.125rem;
623
+ transition: color 0.15s;
624
+ }
625
+ .anahtar-pill-key-remove:hover { color: var(--anahtar-error, #ef4444); }
626
+ .anahtar-pill-key-remove:disabled { opacity: 0.4; }
627
+
628
+ .anahtar-pill-key-add {
629
+ display: flex;
630
+ align-items: center;
631
+ gap: 0.3rem;
632
+ font-size: 0.75rem;
633
+ color: var(--anahtar-pill-icon, #6b7280);
634
+ background: none;
635
+ border: none;
636
+ cursor: pointer;
637
+ padding: 0.2rem 0;
638
+ transition: color 0.15s;
639
+ margin-top: 0.125rem;
640
+ }
641
+ .anahtar-pill-key-add:hover:not(:disabled) { color: var(--anahtar-primary, #3730a3); }
642
+ .anahtar-pill-key-add:disabled { opacity: 0.4; cursor: not-allowed; }
643
+
644
+ .anahtar-pill-error {
645
+ font-size: 0.75rem;
646
+ color: var(--anahtar-error, #ef4444);
647
+ text-align: right;
648
+ padding: 0 0.5rem;
649
+ }
650
+ </style>
@@ -0,0 +1,22 @@
1
+ import { type AuthMessages } from '../i18n/index.js';
2
+ interface PasskeyInfo {
3
+ id: string;
4
+ credentialId?: string;
5
+ name?: string | null;
6
+ createdAt?: number;
7
+ }
8
+ interface Props {
9
+ apiBase?: string;
10
+ user?: {
11
+ email: string;
12
+ } | null;
13
+ locale?: string;
14
+ messages?: Partial<AuthMessages>;
15
+ onSuccess?: () => void | Promise<void>;
16
+ onSignOut?: () => void | Promise<void>;
17
+ onPasskeysChange?: () => void | Promise<void>;
18
+ getPasskeys?: () => Promise<PasskeyInfo[]>;
19
+ }
20
+ declare const AuthPill: import("svelte").Component<Props, {}, "">;
21
+ type AuthPill = ReturnType<typeof AuthPill>;
22
+ export default AuthPill;