@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,204 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { onMount } from 'svelte';
5
+ import type { Plan, PriceOfferSdk } from '@thebridge/auth-core';
6
+ import { getBridgeAuth, loadSubscription, subscriptionStore } from '../../../core/bridge-instance.js';
7
+ import Alert from '../sdk-auth/shared/Alert.svelte';
8
+ import Spinner from '../sdk-auth/shared/Spinner.svelte';
9
+
10
+ interface Props extends HTMLAttributes<HTMLDivElement> {
11
+ successUrl: string;
12
+ cancelUrl: string;
13
+ onSelect?: (detail: { plan: Plan; price: PriceOfferSdk }) => void;
14
+ planCard?: Snippet<[{ plan: Plan; prices: PriceOfferSdk[]; isCurrent: boolean; onPick: (price: PriceOfferSdk) => void }]>;
15
+ emptyState?: Snippet;
16
+ loadingState?: Snippet;
17
+ }
18
+
19
+ let {
20
+ successUrl,
21
+ cancelUrl,
22
+ onSelect,
23
+ planCard,
24
+ emptyState,
25
+ loadingState,
26
+ class: className,
27
+ style,
28
+ ...rest
29
+ }: Props = $props();
30
+
31
+ let picking = $state(false);
32
+ let pickError = $state<string | null>(null);
33
+
34
+ const status = $derived($subscriptionStore.status);
35
+ const plans = $derived($subscriptionStore.plans);
36
+ const loading = $derived($subscriptionStore.loading);
37
+ const storeError = $derived($subscriptionStore.error);
38
+
39
+ type UiState = 'idle' | 'payment-failed' | 'setup-payments' | 'select-plan' | 'active' | 'trial';
40
+
41
+ const uiState = $derived<UiState>((() => {
42
+ if (!status) return 'idle';
43
+ if (status.paymentFailed) return 'payment-failed';
44
+ if (status.shouldSetupPayments) return 'setup-payments';
45
+ if (status.shouldSelectPlan) return 'select-plan';
46
+ if (status.trial) return 'trial';
47
+ if (status.paymentsEnabled || status.plan) return 'active';
48
+ return 'select-plan';
49
+ })());
50
+
51
+ const currentPlanKey = $derived(status?.plan?.key ?? null);
52
+
53
+ onMount(() => {
54
+ // Only load if not already loaded
55
+ if (!$subscriptionStore.status && !$subscriptionStore.loading) {
56
+ loadSubscription();
57
+ }
58
+ });
59
+
60
+ async function handlePick(plan: Plan, price: PriceOfferSdk): Promise<void> {
61
+ picking = true;
62
+ pickError = null;
63
+ try {
64
+ if (price.amount === 0) {
65
+ // Free plan — select directly
66
+ await getBridgeAuth().selectFreePlan(plan.key);
67
+ await loadSubscription();
68
+ onSelect?.({ plan, price });
69
+ } else if (status?.paymentsEnabled) {
70
+ // Already has payment method — change plan
71
+ await getBridgeAuth().changePlan(plan.key, price);
72
+ await loadSubscription();
73
+ onSelect?.({ plan, price });
74
+ } else {
75
+ // Needs checkout — redirect to Stripe (or direct plan set when Stripe not configured)
76
+ const session = await getBridgeAuth().startCheckout(plan.key, price, { successUrl, cancelUrl });
77
+ if (session.sessionId === null) {
78
+ // Stripe not configured — plan was set directly on the backend
79
+ await loadSubscription();
80
+ onSelect?.({ plan, price });
81
+ } else {
82
+ const { loadStripe } = await import('@stripe/stripe-js');
83
+ const stripe = await loadStripe(session.publicKey);
84
+ if (!stripe) throw new Error('Failed to load Stripe');
85
+ await stripe.redirectToCheckout({ sessionId: session.sessionId });
86
+ }
87
+ }
88
+ } catch (err) {
89
+ pickError = err instanceof Error ? err.message : 'Something went wrong';
90
+ } finally {
91
+ picking = false;
92
+ }
93
+ }
94
+
95
+ async function goToPortal(): Promise<void> {
96
+ try {
97
+ const url = await getBridgeAuth().getPortalUrl();
98
+ window.location.href = url;
99
+ } catch (err) {
100
+ pickError = err instanceof Error ? err.message : 'Failed to open billing portal';
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <div
106
+ class={className}
107
+ {style}
108
+ data-bridge-plan-selector
109
+ data-loading={loading || picking}
110
+ data-state={uiState}
111
+ {...rest}
112
+ >
113
+ {#if loading}
114
+ {#if loadingState}
115
+ {@render loadingState()}
116
+ {:else}
117
+ <div class="bridge-plan-loading">
118
+ <Spinner />
119
+ </div>
120
+ {/if}
121
+ {:else if storeError}
122
+ <Alert variant="error">{storeError}</Alert>
123
+ {:else}
124
+ {#if pickError}
125
+ <Alert variant="error">{pickError}</Alert>
126
+ {/if}
127
+
128
+ {#if uiState === 'payment-failed'}
129
+ <div data-bridge-plan-payment-failed class="bridge-plan-payment-failed">
130
+ <Alert variant="error">
131
+ Your last payment failed. Please update your payment method to continue.
132
+ </Alert>
133
+ <button class="bridge-btn-primary bridge-plan-portal-btn" onclick={goToPortal}>
134
+ Manage billing
135
+ </button>
136
+ </div>
137
+ {/if}
138
+
139
+ {#if plans && plans.length === 0}
140
+ {#if emptyState}
141
+ {@render emptyState()}
142
+ {:else}
143
+ <p class="bridge-plan-empty">No plans available.</p>
144
+ {/if}
145
+ {:else if plans}
146
+ <div class="bridge-plan-cards" data-bridge-plan-cards>
147
+ {#each plans as plan (plan.key)}
148
+ {@const isCurrent = plan.key === currentPlanKey}
149
+ {@const onPick = (price: PriceOfferSdk) => handlePick(plan, price)}
150
+
151
+ {#if planCard}
152
+ {@render planCard({ plan, prices: plan.prices, isCurrent, onPick })}
153
+ {:else}
154
+ <div
155
+ data-bridge-plan-card
156
+ data-current={isCurrent}
157
+ data-trial={plan.trial}
158
+ class="bridge-plan-card"
159
+ >
160
+ <div class="bridge-plan-card-header">
161
+ <h3 class="bridge-plan-name">{plan.name}</h3>
162
+ {#if plan.trial && plan.trialDays > 0}
163
+ <span class="bridge-plan-trial-badge">{plan.trialDays}-day trial</span>
164
+ {/if}
165
+ </div>
166
+
167
+ {#if plan.description}
168
+ <p class="bridge-plan-description">{plan.description}</p>
169
+ {/if}
170
+
171
+ <div class="bridge-plan-prices">
172
+ {#each plan.prices as price (price.recurrenceInterval + price.currency)}
173
+ <button
174
+ class="bridge-btn-primary bridge-plan-select-btn"
175
+ disabled={isCurrent || picking}
176
+ onclick={() => onPick(price)}
177
+ >
178
+ {#if isCurrent}
179
+ Current plan
180
+ {:else if price.amount === 0}
181
+ Select free plan
182
+ {:else}
183
+ {price.amount / 100} {price.currency.toUpperCase()} / {price.recurrenceInterval}
184
+ {/if}
185
+ </button>
186
+ {/each}
187
+
188
+ {#if plan.prices.length === 0}
189
+ <button
190
+ class="bridge-btn-primary bridge-plan-select-btn"
191
+ disabled={isCurrent || picking}
192
+ onclick={() => handlePick(plan, { amount: 0, currency: 'usd', recurrenceInterval: 'month' })}
193
+ >
194
+ {isCurrent ? 'Current plan' : 'Select plan'}
195
+ </button>
196
+ {/if}
197
+ </div>
198
+ </div>
199
+ {/if}
200
+ {/each}
201
+ </div>
202
+ {/if}
203
+ {/if}
204
+ </div>
@@ -0,0 +1,22 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Plan, PriceOfferSdk } from '@thebridge/auth-core';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ successUrl: string;
6
+ cancelUrl: string;
7
+ onSelect?: (detail: {
8
+ plan: Plan;
9
+ price: PriceOfferSdk;
10
+ }) => void;
11
+ planCard?: Snippet<[{
12
+ plan: Plan;
13
+ prices: PriceOfferSdk[];
14
+ isCurrent: boolean;
15
+ onPick: (price: PriceOfferSdk) => void;
16
+ }]>;
17
+ emptyState?: Snippet;
18
+ loadingState?: Snippet;
19
+ }
20
+ declare const PlanSelector: import("svelte").Component<Props, {}, "">;
21
+ type PlanSelector = ReturnType<typeof PlanSelector>;
22
+ export default PlanSelector;
@@ -0,0 +1,125 @@
1
+ <script lang="ts">
2
+ import type { TeamUser } from '@thebridge/auth-core';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLDialogAttributes } from 'svelte/elements';
5
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
6
+ import Alert from '../sdk-auth/shared/Alert.svelte';
7
+
8
+ interface Props extends HTMLDialogAttributes {
9
+ open?: boolean;
10
+ onclose?: () => void;
11
+ onadded?: (users: TeamUser[]) => void;
12
+ titleSnippet?: Snippet;
13
+ actions?: Snippet<[{ loading: boolean; onconfirm?: () => void; oncancel?: () => void }]>;
14
+ }
15
+
16
+ let {
17
+ open = false,
18
+ onclose,
19
+ onadded,
20
+ titleSnippet,
21
+ actions,
22
+ class: className,
23
+ style,
24
+ ...rest
25
+ }: Props = $props();
26
+
27
+ let dialogEl: HTMLDialogElement | undefined = $state();
28
+ let emailsText = $state('');
29
+ let loading = $state(false);
30
+ let error = $state<string | null>(null);
31
+
32
+ $effect(() => {
33
+ if (!dialogEl) return;
34
+ if (open && !dialogEl.open) {
35
+ emailsText = '';
36
+ error = null;
37
+ dialogEl.showModal();
38
+ } else if (!open && dialogEl.open) {
39
+ dialogEl.close();
40
+ }
41
+ });
42
+
43
+ async function handleSubmit() {
44
+ const emails = emailsText
45
+ .split(/[,\n]/)
46
+ .map((e) => e.trim())
47
+ .filter(Boolean);
48
+
49
+ if (emails.length === 0) {
50
+ error = 'Please enter at least one email address.';
51
+ return;
52
+ }
53
+
54
+ loading = true;
55
+ error = null;
56
+ try {
57
+ const bridge = getBridgeAuth();
58
+ const created = await bridge.team.createUsers(emails);
59
+ onadded?.(created);
60
+ onclose?.();
61
+ } catch (err) {
62
+ error = err instanceof Error ? err.message : 'Failed to add users';
63
+ } finally {
64
+ loading = false;
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <dialog
70
+ bind:this={dialogEl}
71
+ class={className}
72
+ {style}
73
+ data-bridge-team-dialog
74
+ onclose={() => onclose?.()}
75
+ {...rest}
76
+ >
77
+ <div class="bridge-team-dialog-content">
78
+ {#if titleSnippet}
79
+ {@render titleSnippet()}
80
+ {:else}
81
+ <h3 class="bridge-team-dialog-title">Add Team Members</h3>
82
+ <p class="bridge-team-dialog-subtitle">
83
+ Enter email addresses separated by commas or new lines.
84
+ </p>
85
+ {/if}
86
+
87
+ {#if error}
88
+ <div class="bridge-team-dialog-error">
89
+ <Alert variant="error">{error}</Alert>
90
+ </div>
91
+ {/if}
92
+
93
+ <div class="bridge-team-form-group">
94
+ <label for="bridge-add-emails">Email addresses</label>
95
+ <textarea
96
+ id="bridge-add-emails"
97
+ bind:value={emailsText}
98
+ placeholder="user1@example.com&#10;user2@example.com"
99
+ rows="4"
100
+ disabled={loading}
101
+ ></textarea>
102
+ </div>
103
+
104
+ {#if actions}
105
+ {@render actions({ loading, onconfirm: handleSubmit, oncancel: onclose })}
106
+ {:else}
107
+ <div class="bridge-team-dialog-actions">
108
+ <button
109
+ class="bridge-btn bridge-btn-secondary"
110
+ onclick={() => onclose?.()}
111
+ disabled={loading}
112
+ >
113
+ Cancel
114
+ </button>
115
+ <button
116
+ class="bridge-btn bridge-btn-primary"
117
+ onclick={handleSubmit}
118
+ disabled={loading || !emailsText.trim()}
119
+ >
120
+ {loading ? 'Adding...' : 'Add Members'}
121
+ </button>
122
+ </div>
123
+ {/if}
124
+ </div>
125
+ </dialog>
@@ -0,0 +1,17 @@
1
+ import type { TeamUser } from '@thebridge/auth-core';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLDialogAttributes } from 'svelte/elements';
4
+ interface Props extends HTMLDialogAttributes {
5
+ open?: boolean;
6
+ onclose?: () => void;
7
+ onadded?: (users: TeamUser[]) => void;
8
+ titleSnippet?: Snippet;
9
+ actions?: Snippet<[{
10
+ loading: boolean;
11
+ onconfirm?: () => void;
12
+ oncancel?: () => void;
13
+ }]>;
14
+ }
15
+ declare const TeamAddUserDialog: import("svelte").Component<Props, {}, "">;
16
+ type TeamAddUserDialog = ReturnType<typeof TeamAddUserDialog>;
17
+ export default TeamAddUserDialog;
@@ -0,0 +1,80 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLDialogAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLDialogAttributes {
6
+ open?: boolean;
7
+ title?: string;
8
+ message?: string;
9
+ confirmLabel?: string;
10
+ variant?: 'danger' | 'default';
11
+ loading?: boolean;
12
+ onconfirm?: () => void;
13
+ oncancel?: () => void;
14
+ titleSnippet?: Snippet;
15
+ actions?: Snippet<[{ loading: boolean; onconfirm?: () => void; oncancel?: () => void }]>;
16
+ }
17
+
18
+ let {
19
+ open = false,
20
+ title = 'Confirm',
21
+ message = 'Are you sure?',
22
+ confirmLabel = 'Confirm',
23
+ variant = 'danger' as 'danger' | 'default',
24
+ loading = false,
25
+ onconfirm,
26
+ oncancel,
27
+ titleSnippet,
28
+ actions,
29
+ class: className,
30
+ style,
31
+ ...rest
32
+ }: Props = $props();
33
+
34
+ let dialogEl: HTMLDialogElement | undefined = $state();
35
+
36
+ $effect(() => {
37
+ if (!dialogEl) return;
38
+ if (open && !dialogEl.open) dialogEl.showModal();
39
+ else if (!open && dialogEl.open) dialogEl.close();
40
+ });
41
+ </script>
42
+
43
+ <dialog
44
+ bind:this={dialogEl}
45
+ class={className}
46
+ {style}
47
+ data-bridge-team-dialog
48
+ data-variant={variant}
49
+ onclose={() => oncancel?.()}
50
+ {...rest}
51
+ >
52
+ <div class="bridge-team-dialog-content">
53
+ {#if titleSnippet}
54
+ {@render titleSnippet()}
55
+ {:else}
56
+ <h3 class="bridge-team-dialog-title">{title}</h3>
57
+ <p class="bridge-team-dialog-message">{message}</p>
58
+ {/if}
59
+ {#if actions}
60
+ {@render actions({ loading, onconfirm, oncancel })}
61
+ {:else}
62
+ <div class="bridge-team-dialog-actions">
63
+ <button
64
+ class="bridge-btn bridge-btn-secondary"
65
+ onclick={() => oncancel?.()}
66
+ disabled={loading}
67
+ >
68
+ Cancel
69
+ </button>
70
+ <button
71
+ class="bridge-btn {variant === 'danger' ? 'bridge-btn-danger' : 'bridge-btn-primary'}"
72
+ onclick={() => onconfirm?.()}
73
+ disabled={loading}
74
+ >
75
+ {loading ? 'Processing...' : confirmLabel}
76
+ </button>
77
+ </div>
78
+ {/if}
79
+ </div>
80
+ </dialog>
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLDialogAttributes {
4
+ open?: boolean;
5
+ title?: string;
6
+ message?: string;
7
+ confirmLabel?: string;
8
+ variant?: 'danger' | 'default';
9
+ loading?: boolean;
10
+ onconfirm?: () => void;
11
+ oncancel?: () => void;
12
+ titleSnippet?: Snippet;
13
+ actions?: Snippet<[{
14
+ loading: boolean;
15
+ onconfirm?: () => void;
16
+ oncancel?: () => void;
17
+ }]>;
18
+ }
19
+ declare const TeamConfirmDialog: import("svelte").Component<Props, {}, "">;
20
+ type TeamConfirmDialog = ReturnType<typeof TeamConfirmDialog>;
21
+ export default TeamConfirmDialog;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ import type { TeamUser } from '@thebridge/auth-core';
3
+ import type { Snippet } from 'svelte';
4
+ import type { HTMLDialogAttributes } from 'svelte/elements';
5
+ import { getBridgeAuth } from '../../../core/bridge-instance.js';
6
+ import Alert from '../sdk-auth/shared/Alert.svelte';
7
+
8
+ interface Props extends HTMLDialogAttributes {
9
+ open?: boolean;
10
+ user?: TeamUser | null;
11
+ roles?: string[];
12
+ onclose?: () => void;
13
+ onupdated?: (user: TeamUser) => void;
14
+ titleSnippet?: Snippet;
15
+ actions?: Snippet<[{ loading: boolean; onconfirm?: () => void; oncancel?: () => void }]>;
16
+ }
17
+
18
+ let {
19
+ open = false,
20
+ user = null as TeamUser | null,
21
+ roles = [] as string[],
22
+ onclose,
23
+ onupdated,
24
+ titleSnippet,
25
+ actions,
26
+ class: className,
27
+ style,
28
+ ...rest
29
+ }: Props = $props();
30
+
31
+ let dialogEl: HTMLDialogElement | undefined = $state();
32
+ let selectedRole = $state('');
33
+ let enabled = $state(true);
34
+ let loading = $state(false);
35
+ let error = $state<string | null>(null);
36
+
37
+ $effect(() => {
38
+ if (!dialogEl) return;
39
+ if (open && !dialogEl.open) {
40
+ selectedRole = user?.role ?? '';
41
+ enabled = user?.enabled ?? true;
42
+ error = null;
43
+ dialogEl.showModal();
44
+ } else if (!open && dialogEl.open) {
45
+ dialogEl.close();
46
+ }
47
+ });
48
+
49
+ async function handleSubmit() {
50
+ if (!user) return;
51
+ loading = true;
52
+ error = null;
53
+ try {
54
+ const bridge = getBridgeAuth();
55
+ const updated = await bridge.team.updateUser({
56
+ id: user.id,
57
+ role: selectedRole || undefined,
58
+ enabled,
59
+ });
60
+ onupdated?.(updated);
61
+ onclose?.();
62
+ } catch (err) {
63
+ error = err instanceof Error ? err.message : 'Failed to update user';
64
+ } finally {
65
+ loading = false;
66
+ }
67
+ }
68
+ </script>
69
+
70
+ <dialog
71
+ bind:this={dialogEl}
72
+ class={className}
73
+ {style}
74
+ data-bridge-team-dialog
75
+ onclose={() => onclose?.()}
76
+ {...rest}
77
+ >
78
+ <div class="bridge-team-dialog-content">
79
+ {#if titleSnippet}
80
+ {@render titleSnippet()}
81
+ {:else}
82
+ <h3 class="bridge-team-dialog-title">Edit User</h3>
83
+ {#if user}
84
+ <p class="bridge-team-dialog-subtitle">{user.email}</p>
85
+ {/if}
86
+ {/if}
87
+
88
+ {#if error}
89
+ <div class="bridge-team-dialog-error">
90
+ <Alert variant="error">{error}</Alert>
91
+ </div>
92
+ {/if}
93
+
94
+ <div class="bridge-team-form-group">
95
+ <label for="bridge-edit-role">Role</label>
96
+ <select id="bridge-edit-role" bind:value={selectedRole} disabled={loading}>
97
+ {#each roles as role}
98
+ <option value={role}>{role}</option>
99
+ {/each}
100
+ </select>
101
+ </div>
102
+
103
+ <div class="bridge-team-form-group">
104
+ <label class="bridge-team-checkbox-label">
105
+ <input type="checkbox" bind:checked={enabled} disabled={loading} />
106
+ <span>Enabled</span>
107
+ </label>
108
+ </div>
109
+
110
+ {#if actions}
111
+ {@render actions({ loading, onconfirm: handleSubmit, oncancel: onclose })}
112
+ {:else}
113
+ <div class="bridge-team-dialog-actions">
114
+ <button
115
+ class="bridge-btn bridge-btn-secondary"
116
+ onclick={() => onclose?.()}
117
+ disabled={loading}
118
+ >
119
+ Cancel
120
+ </button>
121
+ <button
122
+ class="bridge-btn bridge-btn-primary"
123
+ onclick={handleSubmit}
124
+ disabled={loading}
125
+ >
126
+ {loading ? 'Saving...' : 'Save Changes'}
127
+ </button>
128
+ </div>
129
+ {/if}
130
+ </div>
131
+ </dialog>
@@ -0,0 +1,19 @@
1
+ import type { TeamUser } from '@thebridge/auth-core';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLDialogAttributes } from 'svelte/elements';
4
+ interface Props extends HTMLDialogAttributes {
5
+ open?: boolean;
6
+ user?: TeamUser | null;
7
+ roles?: string[];
8
+ onclose?: () => void;
9
+ onupdated?: (user: TeamUser) => void;
10
+ titleSnippet?: Snippet;
11
+ actions?: Snippet<[{
12
+ loading: boolean;
13
+ onconfirm?: () => void;
14
+ oncancel?: () => void;
15
+ }]>;
16
+ }
17
+ declare const TeamEditUserDialog: import("svelte").Component<Props, {}, "">;
18
+ type TeamEditUserDialog = ReturnType<typeof TeamEditUserDialog>;
19
+ export default TeamEditUserDialog;
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import TeamProfileForm from './TeamProfileForm.svelte';
5
+ import TeamUserList from './TeamUserList.svelte';
6
+ import TeamWorkspaceForm from './TeamWorkspaceForm.svelte';
7
+
8
+ type Tab = { id: 'users' | 'profile' | 'workspace'; label: string };
9
+
10
+ interface Props extends HTMLAttributes<HTMLDivElement> {
11
+ defaultTab?: 'users' | 'profile' | 'workspace';
12
+ showProfileTab?: boolean;
13
+ showWorkspaceTab?: boolean;
14
+ onError?: (error: Error) => void;
15
+ tabBar?: Snippet<[{ tabs: Tab[]; activeTab: string; setTab: (id: string) => void }]>;
16
+ }
17
+
18
+ let {
19
+ defaultTab = 'users' as 'users' | 'profile' | 'workspace',
20
+ showProfileTab = true,
21
+ showWorkspaceTab = true,
22
+ onError,
23
+ tabBar,
24
+ class: className,
25
+ style,
26
+ ...rest
27
+ }: Props = $props();
28
+
29
+ let activeTab = $state(defaultTab);
30
+
31
+ const tabs = $derived(
32
+ [
33
+ { id: 'users' as const, label: 'Users' },
34
+ showProfileTab && { id: 'profile' as const, label: 'Profile' },
35
+ showWorkspaceTab && { id: 'workspace' as const, label: 'Workspace' },
36
+ ].filter(Boolean) as Tab[],
37
+ );
38
+
39
+ function setTab(id: string) {
40
+ activeTab = id as 'users' | 'profile' | 'workspace';
41
+ }
42
+ </script>
43
+
44
+ <div class={className} {style} data-bridge-team-panel {...rest}>
45
+ {#if tabs.length > 1}
46
+ {#if tabBar}
47
+ {@render tabBar({ tabs, activeTab, setTab })}
48
+ {:else}
49
+ <nav class="bridge-team-tabs">
50
+ {#each tabs as tab (tab.id)}
51
+ <button
52
+ class="bridge-team-tab"
53
+ data-active={activeTab === tab.id}
54
+ onclick={() => setTab(tab.id)}
55
+ >
56
+ {tab.label}
57
+ </button>
58
+ {/each}
59
+ </nav>
60
+ {/if}
61
+ {/if}
62
+
63
+ <div class="bridge-team-tab-content">
64
+ {#if activeTab === 'users'}
65
+ <TeamUserList {onError} />
66
+ {:else if activeTab === 'profile'}
67
+ <TeamProfileForm {onError} />
68
+ {:else if activeTab === 'workspace'}
69
+ <TeamWorkspaceForm {onError} />
70
+ {/if}
71
+ </div>
72
+ </div>