@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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/auth/route-guard.d.ts +2 -0
- package/dist/auth/route-guard.js +28 -0
- package/dist/client/BridgeBootstrap.d.ts +8 -0
- package/dist/client/BridgeBootstrap.js +105 -0
- package/dist/client/BridgeBootstrap.svelte +43 -0
- package/dist/client/BridgeBootstrap.svelte.d.ts +6 -0
- package/dist/client/components/FeatureFlag.svelte +37 -0
- package/dist/client/components/FeatureFlag.svelte.d.ts +15 -0
- package/dist/client/components/ProfileName.svelte +18 -0
- package/dist/client/components/ProfileName.svelte.d.ts +6 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte +365 -0
- package/dist/client/components/developer/ApiTokenManagement.svelte.d.ts +6 -0
- package/dist/client/components/sdk-auth/ForgotPassword.svelte +170 -0
- package/dist/client/components/sdk-auth/ForgotPassword.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/LoginForm.svelte +414 -0
- package/dist/client/components/sdk-auth/LoginForm.svelte.d.ts +25 -0
- package/dist/client/components/sdk-auth/MagicLink.svelte +92 -0
- package/dist/client/components/sdk-auth/MagicLink.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/MfaChallenge.svelte +150 -0
- package/dist/client/components/sdk-auth/MfaChallenge.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/MfaSetup.svelte +182 -0
- package/dist/client/components/sdk-auth/MfaSetup.svelte.d.ts +8 -0
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte +99 -0
- package/dist/client/components/sdk-auth/PasskeyLogin.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte +83 -0
- package/dist/client/components/sdk-auth/PasskeyRequestSetupLink.svelte.d.ts +7 -0
- package/dist/client/components/sdk-auth/PasskeySetup.svelte +151 -0
- package/dist/client/components/sdk-auth/PasskeySetup.svelte.d.ts +11 -0
- package/dist/client/components/sdk-auth/SignupForm.svelte +119 -0
- package/dist/client/components/sdk-auth/SignupForm.svelte.d.ts +14 -0
- package/dist/client/components/sdk-auth/SsoButton.svelte +70 -0
- package/dist/client/components/sdk-auth/SsoButton.svelte.d.ts +13 -0
- package/dist/client/components/sdk-auth/SsoProviderIcon.svelte +56 -0
- package/dist/client/components/sdk-auth/SsoProviderIcon.svelte.d.ts +7 -0
- package/dist/client/components/sdk-auth/TenantSelector.svelte +83 -0
- package/dist/client/components/sdk-auth/TenantSelector.svelte.d.ts +11 -0
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte +108 -0
- package/dist/client/components/sdk-auth/WorkspaceSelector.svelte.d.ts +16 -0
- package/dist/client/components/sdk-auth/shared/Alert.svelte +17 -0
- package/dist/client/components/sdk-auth/shared/Alert.svelte.d.ts +9 -0
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte +23 -0
- package/dist/client/components/sdk-auth/shared/AuthFormWrapper.svelte.d.ts +10 -0
- package/dist/client/components/sdk-auth/shared/Spinner.svelte +16 -0
- package/dist/client/components/sdk-auth/shared/Spinner.svelte.d.ts +7 -0
- package/dist/client/components/subscription/PlanSelector.svelte +204 -0
- package/dist/client/components/subscription/PlanSelector.svelte.d.ts +22 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte +125 -0
- package/dist/client/components/team/TeamAddUserDialog.svelte.d.ts +17 -0
- package/dist/client/components/team/TeamConfirmDialog.svelte +80 -0
- package/dist/client/components/team/TeamConfirmDialog.svelte.d.ts +21 -0
- package/dist/client/components/team/TeamEditUserDialog.svelte +131 -0
- package/dist/client/components/team/TeamEditUserDialog.svelte.d.ts +19 -0
- package/dist/client/components/team/TeamManagementPanel.svelte +72 -0
- package/dist/client/components/team/TeamManagementPanel.svelte.d.ts +20 -0
- package/dist/client/components/team/TeamProfileForm.svelte +129 -0
- package/dist/client/components/team/TeamProfileForm.svelte.d.ts +7 -0
- package/dist/client/components/team/TeamUserActionsMenu.svelte +63 -0
- package/dist/client/components/team/TeamUserActionsMenu.svelte.d.ts +9 -0
- package/dist/client/components/team/TeamUserList.svelte +226 -0
- package/dist/client/components/team/TeamUserList.svelte.d.ts +7 -0
- package/dist/client/components/team/TeamWorkspaceForm.svelte +134 -0
- package/dist/client/components/team/TeamWorkspaceForm.svelte.d.ts +7 -0
- package/dist/client/stores/config.store.d.ts +6 -0
- package/dist/client/stores/config.store.js +76 -0
- package/dist/client/utils/cn.d.ts +1 -0
- package/dist/client/utils/cn.js +3 -0
- package/dist/core/bridge-instance.d.ts +52 -0
- package/dist/core/bridge-instance.js +160 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +39 -0
- package/dist/shared/feature-flag.d.ts +6 -0
- package/dist/shared/feature-flag.js +13 -0
- package/dist/shared/logger.d.ts +18 -0
- package/dist/shared/logger.js +32 -0
- package/dist/shared/profile.d.ts +18 -0
- package/dist/shared/profile.js +25 -0
- package/dist/shared/types/config.d.ts +42 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/styles.css +1089 -0
- 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 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>
|