@nebulr-group/bridge-svelte 0.1.0-beta.0

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -0
  3. package/dist/auth/route-guard.d.ts +2 -0
  4. package/dist/auth/route-guard.js +28 -0
  5. package/dist/client/BridgeBootstrap.d.ts +8 -0
  6. package/dist/client/BridgeBootstrap.js +105 -0
  7. package/dist/client/BridgeBootstrap.svelte +43 -0
  8. package/dist/client/BridgeBootstrap.svelte.d.ts +6 -0
  9. package/dist/client/components/FeatureFlag.svelte +37 -0
  10. package/dist/client/components/FeatureFlag.svelte.d.ts +15 -0
  11. package/dist/client/components/ProfileName.svelte +18 -0
  12. package/dist/client/components/ProfileName.svelte.d.ts +6 -0
  13. package/dist/client/components/developer/ApiTokenManagement.svelte +365 -0
  14. package/dist/client/components/developer/ApiTokenManagement.svelte.d.ts +6 -0
  15. package/dist/client/components/sdk-auth/ForgotPassword.svelte +170 -0
  16. package/dist/client/components/sdk-auth/ForgotPassword.svelte.d.ts +10 -0
  17. package/dist/client/components/sdk-auth/LoginForm.svelte +414 -0
  18. package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +25 -0
  19. package/dist/client/components/sdk-auth/MagicLink.svelte +92 -0
  20. package/dist/client/components/sdk-auth/MagicLink.svelte.d.ts +9 -0
  21. package/dist/client/components/sdk-auth/MfaChallenge.svelte +150 -0
  22. package/dist/client/components/sdk-auth/MfaChallenge.svelte.d.ts +9 -0
  23. package/dist/client/components/sdk-auth/MfaSetup.svelte +182 -0
  24. package/dist/client/components/sdk-auth/MfaSetup.svelte.d.ts +8 -0
  25. package/dist/client/components/sdk-auth/PasskeyLogin.svelte +99 -0
  26. package/dist/client/components/sdk-auth/PasskeyLogin.svelte.d.ts +10 -0
  27. package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte +83 -0
  28. package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte.d.ts +7 -0
  29. package/dist/client/components/sdk-auth/PasskeySetup.svelte +151 -0
  30. package/dist/client/components/sdk-auth/PasskeySetup.svelte.d.ts +11 -0
  31. package/dist/client/components/sdk-auth/SignupForm.svelte +119 -0
  32. package/dist/client/components/sdk-auth/SignupForm.svelte.d.ts +14 -0
  33. package/dist/client/components/sdk-auth/SsoButton.svelte +70 -0
  34. package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +13 -0
  35. package/dist/client/components/sdk-auth/SsoProviderIcon.svelte +56 -0
  36. package/dist/client/components/sdk-auth/SsoProviderIcon.svelte.d.ts +7 -0
  37. package/dist/client/components/sdk-auth/TenantSelector.svelte +83 -0
  38. package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +11 -0
  39. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +108 -0
  40. package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +16 -0
  41. package/dist/client/components/sdk-auth/shared/Alert.svelte +17 -0
  42. package/dist/client/components/sdk-auth/shared/Alert.svelte.d.ts +9 -0
  43. package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte +23 -0
  44. package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte.d.ts +10 -0
  45. package/dist/client/components/sdk-auth/shared/Spinner.svelte +16 -0
  46. package/dist/client/components/sdk-auth/shared/Spinner.svelte.d.ts +7 -0
  47. package/dist/client/components/subscription/PlanSelector.svelte +204 -0
  48. package/dist/client/components/subscription/PlanSelector.svelte.d.ts +22 -0
  49. package/dist/client/components/team/TeamAddUserDialog.svelte +125 -0
  50. package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +17 -0
  51. package/dist/client/components/team/TeamConfirmDialog.svelte +80 -0
  52. package/dist/client/components/team/TeamConfirmDialog.svelte.d.ts +21 -0
  53. package/dist/client/components/team/TeamEditUserDialog.svelte +131 -0
  54. package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +19 -0
  55. package/dist/client/components/team/TeamManagementPanel.svelte +72 -0
  56. package/dist/client/components/team/TeamManagementPanel.svelte.d.ts +20 -0
  57. package/dist/client/components/team/TeamProfileForm.svelte +129 -0
  58. package/dist/client/components/team/TeamProfileForm.svelte.d.ts +7 -0
  59. package/dist/client/components/team/TeamUserActionsMenu.svelte +63 -0
  60. package/dist/client/components/team/TeamUserActionsMenu.svelte.d.ts +9 -0
  61. package/dist/client/components/team/TeamUserList.svelte +226 -0
  62. package/dist/client/components/team/TeamUserList.svelte.d.ts +7 -0
  63. package/dist/client/components/team/TeamWorkspaceForm.svelte +134 -0
  64. package/dist/client/components/team/TeamWorkspaceForm.svelte.d.ts +7 -0
  65. package/dist/client/stores/config.store.d.ts +6 -0
  66. package/dist/client/stores/config.store.js +76 -0
  67. package/dist/client/utils/cn.d.ts +1 -0
  68. package/dist/client/utils/cn.js +3 -0
  69. package/dist/core/bridge-instance.d.ts +52 -0
  70. package/dist/core/bridge-instance.js +160 -0
  71. package/dist/index.d.ts +34 -0
  72. package/dist/index.js +39 -0
  73. package/dist/shared/feature-flag.d.ts +6 -0
  74. package/dist/shared/feature-flag.js +13 -0
  75. package/dist/shared/logger.d.ts +18 -0
  76. package/dist/shared/logger.js +32 -0
  77. package/dist/shared/profile.d.ts +18 -0
  78. package/dist/shared/profile.js +25 -0
  79. package/dist/shared/types/config.d.ts +42 -0
  80. package/dist/shared/types/config.js +1 -0
  81. package/dist/styles.css +1089 -0
  82. package/package.json +86 -0
@@ -0,0 +1,365 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
4
+ import type { ApiToken, AvailablePrivilege, CreateApiTokenInput } from '@thebridge/auth-core';
5
+
6
+ interface Props extends HTMLAttributes<HTMLDivElement> {}
7
+
8
+ let { class: className, style, ...rest }: Props = $props();
9
+
10
+ // ─── State ─────────────────────────────────────────────────────────────────
11
+
12
+ let tokens = $state<ApiToken[]>([]);
13
+ let loading = $state(true);
14
+ let error = $state<string | null>(null);
15
+ let success = $state<string | null>(null);
16
+
17
+ // Create form
18
+ let showCreateForm = $state(false);
19
+ let creating = $state(false);
20
+ let createName = $state('');
21
+ let createExpiry = $state('');
22
+
23
+ // Privilege picker
24
+ let availablePrivileges = $state<AvailablePrivilege[]>([]);
25
+ let selectedPrivileges = $state<string[]>([]);
26
+ let privSearch = $state('');
27
+ let privDropdownOpen = $state(false);
28
+
29
+ // Show-once token display
30
+ let newToken = $state<string | null>(null);
31
+ let showToken = $state(false);
32
+
33
+ // Revoke confirmation
34
+ let revokeTarget = $state<ApiToken | null>(null);
35
+ let revokeDialogOpen = $state(false);
36
+ let revoking = $state(false);
37
+
38
+ // ─── Load ───────────────────────────────────────────────────────────────────
39
+
40
+ async function loadTokens() {
41
+ loading = true;
42
+ error = null;
43
+ try {
44
+ tokens = await getBridgeAuth().apiTokens.listAppTokens();
45
+ } catch (err) {
46
+ error = err instanceof Error ? err.message : 'Failed to load API tokens';
47
+ } finally {
48
+ loading = false;
49
+ }
50
+ }
51
+
52
+ $effect(() => { loadTokens(); });
53
+
54
+ // ─── Load privileges ────────────────────────────────────────────────────────
55
+
56
+ async function loadPrivileges() {
57
+ try {
58
+ availablePrivileges = await getBridgeAuth().apiTokens.listAvailablePrivileges();
59
+ } catch { /* non-fatal — picker shows empty */ }
60
+ }
61
+
62
+ $effect(() => { if (showCreateForm) loadPrivileges(); });
63
+
64
+ // ─── Create ─────────────────────────────────────────────────────────────────
65
+
66
+ async function createToken() {
67
+ creating = true;
68
+ error = null;
69
+ try {
70
+ const input: CreateApiTokenInput = {
71
+ name: createName.trim(),
72
+ privileges: selectedPrivileges,
73
+ expireAt: createExpiry || undefined,
74
+ };
75
+ const result = await getBridgeAuth().apiTokens.createAppToken(input);
76
+ newToken = result.token;
77
+ tokens = [result.record, ...tokens];
78
+ showCreateForm = false;
79
+ resetCreateForm();
80
+ success = 'Token created. Copy it now — it will not be shown again.';
81
+ } catch (err) {
82
+ error = err instanceof Error ? err.message : 'Failed to create token';
83
+ } finally {
84
+ creating = false;
85
+ }
86
+ }
87
+
88
+ function resetCreateForm() {
89
+ createName = '';
90
+ selectedPrivileges = [];
91
+ privSearch = '';
92
+ privDropdownOpen = false;
93
+ createExpiry = '';
94
+ }
95
+
96
+ // ─── Revoke ─────────────────────────────────────────────────────────────────
97
+
98
+ async function confirmRevoke() {
99
+ if (!revokeTarget) return;
100
+ revoking = true;
101
+ error = null;
102
+ try {
103
+ await getBridgeAuth().apiTokens.revokeAppToken(revokeTarget.id);
104
+ tokens = tokens.filter((t) => t.id !== revokeTarget!.id);
105
+ success = `Token "${revokeTarget.name}" revoked.`;
106
+ revokeDialogOpen = false;
107
+ revokeTarget = null;
108
+ } catch (err) {
109
+ error = err instanceof Error ? err.message : 'Failed to revoke token';
110
+ } finally {
111
+ revoking = false;
112
+ }
113
+ }
114
+
115
+ // ─── Clipboard ──────────────────────────────────────────────────────────────
116
+
117
+ async function copyToClipboard(text: string) {
118
+ try {
119
+ await navigator.clipboard.writeText(text);
120
+ success = 'Copied to clipboard!';
121
+ setTimeout(() => (success = null), 3000);
122
+ } catch {
123
+ error = 'Failed to copy to clipboard';
124
+ }
125
+ }
126
+
127
+ // ─── Click-outside: close privilege dropdown ─────────────────────────────────
128
+
129
+ $effect(() => {
130
+ if (!privDropdownOpen) return;
131
+ function handleOutsideClick(e: MouseEvent) {
132
+ const picker = document.querySelector('.bridge-privilege-picker');
133
+ if (picker && !picker.contains(e.target as Node)) {
134
+ privDropdownOpen = false;
135
+ }
136
+ }
137
+ document.addEventListener('click', handleOutsideClick);
138
+ return () => document.removeEventListener('click', handleOutsideClick);
139
+ });
140
+
141
+ // ─── Helpers ────────────────────────────────────────────────────────────────
142
+
143
+ function formatDate(iso: string | null | undefined): string {
144
+ if (!iso) return '—';
145
+ return new Date(iso).toLocaleDateString();
146
+ }
147
+ </script>
148
+
149
+ <div class={className} {style} data-bridge-api-tokens data-loading={loading} data-creating={creating} {...rest}>
150
+ <!-- Header -->
151
+ <div class="bridge-api-header">
152
+ <div>
153
+ <h2 class="bridge-api-title">API Tokens</h2>
154
+ <p class="bridge-api-subtitle">
155
+ Long-lived JWT tokens for programmatic API access.
156
+ </p>
157
+ </div>
158
+ <button
159
+ class="bridge-btn bridge-btn-primary"
160
+ onclick={() => { showCreateForm = !showCreateForm; if (!showCreateForm) resetCreateForm(); }}
161
+ >
162
+ {showCreateForm ? '✕ Cancel' : '+ Create Token'}
163
+ </button>
164
+ </div>
165
+
166
+ <!-- Error / Success banners -->
167
+ {#if error}
168
+ <div class="bridge-api-error-banner">{error}</div>
169
+ {/if}
170
+ {#if success}
171
+ <div class="bridge-api-success-banner">{success}</div>
172
+ {/if}
173
+
174
+ <!-- Show-once token display -->
175
+ {#if newToken}
176
+ <div class="bridge-api-new-token-banner">
177
+ <p class="bridge-api-new-token-warning">
178
+ ⚠ Store this token securely — you won't be able to see it again.
179
+ </p>
180
+ <div class="bridge-api-new-token-row">
181
+ <input
182
+ class="bridge-input bridge-api-token-input"
183
+ type={showToken ? 'text' : 'password'}
184
+ readonly
185
+ value={newToken}
186
+ />
187
+ <button
188
+ class="bridge-btn-outline"
189
+ onclick={() => (showToken = !showToken)}
190
+ title={showToken ? 'Hide token' : 'Show token'}
191
+ >
192
+ {showToken ? '🙈' : '👁'}
193
+ </button>
194
+ <button
195
+ class="bridge-btn-outline"
196
+ onclick={() => copyToClipboard(newToken!)}
197
+ >
198
+ Copy
199
+ </button>
200
+ </div>
201
+ <button
202
+ class="bridge-btn-outline"
203
+ onclick={() => { newToken = null; showToken = false; }}
204
+ >
205
+ Dismiss
206
+ </button>
207
+ </div>
208
+ {/if}
209
+
210
+ <!-- Inline create form -->
211
+ {#if showCreateForm}
212
+ <div class="bridge-api-inline-form">
213
+ <h3 class="bridge-api-inline-form-title">New API Token</h3>
214
+ <form
215
+ class="bridge-api-inline-form-fields"
216
+ onsubmit={(e) => { e.preventDefault(); createToken(); }}
217
+ >
218
+ <div class="bridge-api-inline-form-row">
219
+ <div class="bridge-api-inline-field">
220
+ <label class="bridge-label" for="token-name">Name <span class="bridge-api-required">*</span></label>
221
+ <input
222
+ id="token-name"
223
+ class="bridge-input"
224
+ type="text"
225
+ placeholder="e.g. CI pipeline token"
226
+ required
227
+ bind:value={createName}
228
+ />
229
+ </div>
230
+ <div class="bridge-api-inline-field">
231
+ <label class="bridge-label">Privileges</label>
232
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
233
+ <div class="bridge-privilege-picker" data-open={privDropdownOpen}>
234
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
235
+ <div class="bridge-privilege-chips" onclick={() => (privDropdownOpen = !privDropdownOpen)}>
236
+ {#each selectedPrivileges as priv (priv)}
237
+ <span class="bridge-privilege-chip">
238
+ {priv}
239
+ <button type="button" onclick={(e) => { e.stopPropagation(); selectedPrivileges = selectedPrivileges.filter((p) => p !== priv); }}>×</button>
240
+ </span>
241
+ {/each}
242
+ {#if !selectedPrivileges.length}
243
+ <span class="bridge-privilege-placeholder">Select privileges…</span>
244
+ {/if}
245
+ </div>
246
+ {#if privDropdownOpen}
247
+ <div class="bridge-privilege-dropdown">
248
+ <input
249
+ class="bridge-privilege-search"
250
+ type="text"
251
+ placeholder="Search…"
252
+ bind:value={privSearch}
253
+ onclick={(e) => e.stopPropagation()}
254
+ />
255
+ {#each availablePrivileges.filter((p) => !selectedPrivileges.includes(p.key) && p.key.toLowerCase().includes(privSearch.toLowerCase())) as priv (priv.key)}
256
+ <button type="button" class="bridge-privilege-option"
257
+ onclick={(e) => { e.stopPropagation(); selectedPrivileges = [...selectedPrivileges, priv.key]; privSearch = ''; }}>
258
+ <span class="bridge-privilege-option-key">{priv.key}</span>
259
+ {#if priv.description}<span class="bridge-privilege-option-desc">{priv.description}</span>{/if}
260
+ </button>
261
+ {:else}
262
+ <div class="bridge-privilege-empty">No privileges found</div>
263
+ {/each}
264
+ </div>
265
+ {/if}
266
+ </div>
267
+ </div>
268
+ <div class="bridge-api-inline-field bridge-api-inline-field--narrow">
269
+ <label class="bridge-label" for="token-expiry">Expiry (optional)</label>
270
+ <input
271
+ id="token-expiry"
272
+ class="bridge-input"
273
+ type="date"
274
+ bind:value={createExpiry}
275
+ />
276
+ </div>
277
+ </div>
278
+ <div class="bridge-api-inline-form-actions">
279
+ <button
280
+ type="submit"
281
+ class="bridge-btn bridge-btn-primary"
282
+ disabled={creating || !createName.trim()}
283
+ >
284
+ {creating ? 'Creating…' : 'Create Token'}
285
+ </button>
286
+ </div>
287
+ </form>
288
+ </div>
289
+ {/if}
290
+
291
+ <!-- Token table -->
292
+ <div class="bridge-api-table-wrapper">
293
+ {#if loading}
294
+ <div class="bridge-api-empty">Loading…</div>
295
+ {:else if tokens.length === 0}
296
+ <div class="bridge-api-empty">
297
+ No API tokens yet. Create one to get started.
298
+ </div>
299
+ {:else}
300
+ <table class="bridge-api-table">
301
+ <thead class="bridge-api-table-head">
302
+ <tr>
303
+ <th class="bridge-api-th">Name</th>
304
+ <th class="bridge-api-th">Privileges</th>
305
+ <th class="bridge-api-th">Created</th>
306
+ <th class="bridge-api-th">Expires</th>
307
+ <th class="bridge-api-th"></th>
308
+ </tr>
309
+ </thead>
310
+ <tbody>
311
+ {#each tokens as token (token.id)}
312
+ <tr class="bridge-api-row">
313
+ <td class="bridge-api-td-name">{token.name}</td>
314
+ <td class="bridge-api-td">
315
+ <div class="bridge-api-privileges">
316
+ {#each token.privileges as priv (priv)}
317
+ <span class="bridge-badge">{priv}</span>
318
+ {/each}
319
+ </div>
320
+ </td>
321
+ <td class="bridge-api-td-muted">{formatDate(token.createdAt)}</td>
322
+ <td class="bridge-api-td-muted">{formatDate(token.expireAt)}</td>
323
+ <td class="bridge-api-td-actions">
324
+ <button
325
+ class="bridge-btn-danger-sm"
326
+ onclick={() => { revokeTarget = token; revokeDialogOpen = true; }}
327
+ >
328
+ Revoke
329
+ </button>
330
+ </td>
331
+ </tr>
332
+ {/each}
333
+ </tbody>
334
+ </table>
335
+ {/if}
336
+ </div>
337
+ </div>
338
+
339
+ <!-- Revoke Confirmation Dialog -->
340
+ {#if revokeDialogOpen}
341
+ <div
342
+ class="bridge-dialog-overlay"
343
+ role="presentation"
344
+ onclick={() => { revokeDialogOpen = false; revokeTarget = null; }}
345
+ ></div>
346
+ <div class="bridge-dialog" role="dialog" aria-modal="true">
347
+ <h3 class="bridge-dialog-title">Revoke token?</h3>
348
+ <p class="bridge-dialog-body">
349
+ Token <strong>{revokeTarget?.name}</strong> will be permanently revoked and can no longer be used.
350
+ </p>
351
+ <div class="bridge-dialog-footer">
352
+ <button
353
+ class="bridge-btn-outline"
354
+ onclick={() => { revokeDialogOpen = false; revokeTarget = null; }}
355
+ >Cancel</button>
356
+ <button
357
+ class="bridge-btn bridge-btn-danger"
358
+ disabled={revoking}
359
+ onclick={confirmRevoke}
360
+ >
361
+ {revoking ? 'Revoking…' : 'Revoke'}
362
+ </button>
363
+ </div>
364
+ </div>
365
+ {/if}
@@ -0,0 +1,6 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ }
4
+ declare const ApiTokenManagement: import("svelte").Component<Props, {}, "">;
5
+ type ApiTokenManagement = ReturnType<typeof ApiTokenManagement>;
6
+ export default ApiTokenManagement;
@@ -0,0 +1,170 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
4
+ import AuthFormWrapper from './shared/AuthFormWrapper.svelte';
5
+ import Spinner from './shared/Spinner.svelte';
6
+ import Alert from './shared/Alert.svelte';
7
+
8
+ interface Props extends HTMLAttributes<HTMLDivElement> {
9
+ token?: string;
10
+ onComplete?: () => void;
11
+ onError?: (error: Error) => void;
12
+ loginHref?: string;
13
+ }
14
+
15
+ let {
16
+ token,
17
+ onComplete,
18
+ onError,
19
+ loginHref = '/login',
20
+ class: className,
21
+ style,
22
+ ...rest
23
+ }: Props = $props();
24
+
25
+ let email = $state('');
26
+ let password = $state('');
27
+ let confirmPassword = $state('');
28
+ let loading = $state(false);
29
+ let error = $state<string | null>(null);
30
+ let emailSent = $state(false);
31
+ let passwordReset = $state(false);
32
+ let showPasswords = $state(false);
33
+
34
+ const isSetMode = $derived(!!token);
35
+
36
+ async function handleSendLink() {
37
+ if (loading) return;
38
+ error = null;
39
+ loading = true;
40
+ try {
41
+ await getBridgeAuth().sendResetPasswordLink(email);
42
+ emailSent = true;
43
+ } catch (err: any) {
44
+ error = err.message || 'Failed to send reset link.';
45
+ onError?.(err);
46
+ } finally {
47
+ loading = false;
48
+ }
49
+ }
50
+
51
+ async function handleSetPassword() {
52
+ if (loading) return;
53
+ error = null;
54
+
55
+ if (password !== confirmPassword) {
56
+ error = 'Passwords do not match.';
57
+ return;
58
+ }
59
+
60
+ if (password.length < 8) {
61
+ error = 'Password must be at least 8 characters.';
62
+ return;
63
+ }
64
+
65
+ loading = true;
66
+ try {
67
+ await getBridgeAuth().updatePassword(token!, password);
68
+ passwordReset = true;
69
+ onComplete?.();
70
+ } catch (err: any) {
71
+ error = err.message || 'Failed to update password.';
72
+ onError?.(err);
73
+ } finally {
74
+ loading = false;
75
+ }
76
+ }
77
+ </script>
78
+
79
+ <AuthFormWrapper
80
+ heading={isSetMode ? 'Set new password' : 'Reset your password'}
81
+ class={className}
82
+ {style}
83
+ {...rest}
84
+ >
85
+ {#if error}
86
+ <Alert variant="error">{error}</Alert>
87
+ {/if}
88
+
89
+ {#if isSetMode}
90
+ {#if passwordReset}
91
+ <h2 class="bridge-success-heading">Password set</h2>
92
+ <div class="bridge-form-footer">
93
+ <a href={loginHref}>Back to login</a>
94
+ </div>
95
+ {:else}
96
+ <form onsubmit={(e) => { e.preventDefault(); handleSetPassword(); }}>
97
+ <div class="bridge-form-group">
98
+ <label for="newPassword">New password</label>
99
+ <div class="bridge-password-wrapper">
100
+ <input
101
+ id="newPassword"
102
+ type={showPasswords ? 'text' : 'password'}
103
+ placeholder="At least 8 characters"
104
+ required
105
+ bind:value={password}
106
+ disabled={loading}
107
+ />
108
+ <button
109
+ type="button"
110
+ class="bridge-password-toggle"
111
+ onclick={() => showPasswords = !showPasswords}
112
+ tabindex={-1}
113
+ aria-label={showPasswords ? 'Hide passwords' : 'Show passwords'}
114
+ >
115
+ {#if showPasswords}
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
117
+ {:else}
118
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
119
+ {/if}
120
+ </button>
121
+ </div>
122
+ </div>
123
+ <div class="bridge-form-group">
124
+ <label for="confirmPassword">Confirm password</label>
125
+ <div class="bridge-password-wrapper">
126
+ <input
127
+ id="confirmPassword"
128
+ type={showPasswords ? 'text' : 'password'}
129
+ placeholder="Repeat password"
130
+ required
131
+ bind:value={confirmPassword}
132
+ disabled={loading}
133
+ />
134
+ </div>
135
+ </div>
136
+ <button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !password}>
137
+ {#if loading}<Spinner size={16} />{:else}Set a password{/if}
138
+ </button>
139
+ </form>
140
+ {/if}
141
+ {:else}
142
+ {#if emailSent}
143
+ <Alert variant="success">Check your email for a password reset link.</Alert>
144
+ <div class="bridge-form-footer">
145
+ <a href={loginHref}>Back to login</a>
146
+ </div>
147
+ {:else}
148
+ <p class="bridge-step-desc">Enter your email and we'll send you a link to reset your password.</p>
149
+ <form onsubmit={(e) => { e.preventDefault(); handleSendLink(); }}>
150
+ <div class="bridge-form-group">
151
+ <label for="reset-email">Email</label>
152
+ <input
153
+ id="reset-email"
154
+ type="email"
155
+ placeholder="you@example.com"
156
+ required
157
+ bind:value={email}
158
+ disabled={loading}
159
+ />
160
+ </div>
161
+ <button type="submit" class="bridge-btn bridge-btn-primary" disabled={loading || !email.trim()}>
162
+ {#if loading}<Spinner size={16} />{:else}Send reset link{/if}
163
+ </button>
164
+ </form>
165
+ <div class="bridge-form-footer">
166
+ <a href={loginHref}>Back to login</a>
167
+ </div>
168
+ {/if}
169
+ {/if}
170
+ </AuthFormWrapper>
@@ -0,0 +1,10 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ interface Props extends HTMLAttributes<HTMLDivElement> {
3
+ token?: string;
4
+ onComplete?: () => void;
5
+ onError?: (error: Error) => void;
6
+ loginHref?: string;
7
+ }
8
+ declare const ForgotPassword: import("svelte").Component<Props, {}, "">;
9
+ type ForgotPassword = ReturnType<typeof ForgotPassword>;
10
+ export default ForgotPassword;