@makolabs/ripple 3.0.10 → 3.0.11

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.
@@ -20,6 +20,7 @@ import type { User, GetUsersOptions, GetUsersResult } from '../index.js';
20
20
  */
21
21
  export declare function resetState(options?: {
22
22
  initialUsers?: User[];
23
+ initialPendingUsers?: User[];
23
24
  simulateDelay?: boolean;
24
25
  delayMs?: number;
25
26
  }): Promise<void>;
@@ -77,3 +78,42 @@ export declare function generateApiKey(options: {
77
78
  apiKey: string;
78
79
  message: string;
79
80
  }>;
81
+ /**
82
+ * Verify an API key (mock implementation).
83
+ * Matches UserManagementAdapter.verifyToken signature.
84
+ *
85
+ * The mock only accepts keys that exactly match an existing active user's
86
+ * `private_metadata.mako_api_key`; unknown/revoked keys are rejected so the
87
+ * failure UI is reachable.
88
+ */
89
+ export declare function verifyToken(options: {
90
+ apiKey: string;
91
+ }): Promise<{
92
+ valid: boolean;
93
+ scopes?: string[];
94
+ error?: string;
95
+ sub?: string;
96
+ client_id?: string;
97
+ }>;
98
+ /**
99
+ * List users awaiting approval (registered in identity provider but not
100
+ * yet onboarded — no org / no API key).
101
+ * Matches UserManagementAdapter.getPendingUsers signature.
102
+ */
103
+ export declare function getPendingUsers(options: GetUsersOptions): Promise<GetUsersResult>;
104
+ /**
105
+ * Approve a pending user — assigns the chosen role and generates an API key.
106
+ * Moves the user from the pending list to the active list.
107
+ * Matches UserManagementAdapter.approveUser signature.
108
+ */
109
+ export declare function approveUser(input: {
110
+ userId: string;
111
+ role: string;
112
+ }): Promise<{
113
+ apiKey: string;
114
+ }>;
115
+ /**
116
+ * Reject a pending user — permanently removes them from the identity provider.
117
+ * Matches UserManagementAdapter.rejectUser signature.
118
+ */
119
+ export declare function rejectUser(userId: string): Promise<void>;
@@ -16,13 +16,17 @@
16
16
  */
17
17
  // Internal module-level state
18
18
  let mockUsers = [];
19
+ let mockPendingUsers = [];
19
20
  let simulateDelay = false;
20
21
  let delayMs = 300;
21
22
  /**
22
23
  * Reset mock adapter state
23
24
  */
24
25
  export async function resetState(options = {}) {
25
- mockUsers = options.initialUsers || [];
26
+ // Deep-clone fixtures so internal mutations (splice in approveUser/rejectUser)
27
+ // don't poison caller-owned arrays — keeps tests order-independent.
28
+ mockUsers = structuredClone(options.initialUsers ?? []);
29
+ mockPendingUsers = structuredClone(options.initialPendingUsers ?? []);
26
30
  simulateDelay = options.simulateDelay ?? false;
27
31
  delayMs = options.delayMs ?? 300;
28
32
  }
@@ -214,3 +218,83 @@ export async function generateApiKey(options) {
214
218
  : 'API key generated successfully'
215
219
  };
216
220
  }
221
+ /**
222
+ * Verify an API key (mock implementation).
223
+ * Matches UserManagementAdapter.verifyToken signature.
224
+ *
225
+ * The mock only accepts keys that exactly match an existing active user's
226
+ * `private_metadata.mako_api_key`; unknown/revoked keys are rejected so the
227
+ * failure UI is reachable.
228
+ */
229
+ export async function verifyToken(options) {
230
+ await delay();
231
+ const owner = mockUsers.find((u) => u.private_metadata &&
232
+ typeof u.private_metadata === 'object' &&
233
+ 'mako_api_key' in u.private_metadata &&
234
+ u.private_metadata.mako_api_key === options.apiKey);
235
+ if (!owner) {
236
+ return {
237
+ valid: false,
238
+ error: 'API key has been revoked or is malformed'
239
+ };
240
+ }
241
+ // Real Mako Auth returns `sub` as the user's email — mirror that here so
242
+ // downstream code that key-lookups by email behaves identically.
243
+ return {
244
+ valid: true,
245
+ sub: owner.email_addresses?.[0]?.email_address ?? owner.id,
246
+ client_id: 'mock-client',
247
+ scopes: owner.permissions ?? []
248
+ };
249
+ }
250
+ /**
251
+ * List users awaiting approval (registered in identity provider but not
252
+ * yet onboarded — no org / no API key).
253
+ * Matches UserManagementAdapter.getPendingUsers signature.
254
+ */
255
+ export async function getPendingUsers(options) {
256
+ await delay();
257
+ const start = (options.page - 1) * options.pageSize;
258
+ const end = start + options.pageSize;
259
+ return {
260
+ users: mockPendingUsers.slice(start, end),
261
+ totalUsers: mockPendingUsers.length
262
+ };
263
+ }
264
+ /**
265
+ * Approve a pending user — assigns the chosen role and generates an API key.
266
+ * Moves the user from the pending list to the active list.
267
+ * Matches UserManagementAdapter.approveUser signature.
268
+ */
269
+ export async function approveUser(input) {
270
+ await delay();
271
+ // Fail fast on empty role — no key minted, no state mutation.
272
+ if (!input.role?.trim()) {
273
+ throw new Error('Role is required to approve a user');
274
+ }
275
+ const idx = mockPendingUsers.findIndex((u) => u.id === input.userId);
276
+ if (idx === -1) {
277
+ throw new Error(`Pending user ${input.userId} not found`);
278
+ }
279
+ const user = mockPendingUsers[idx];
280
+ const apiKey = `mock_api_key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
281
+ mockPendingUsers.splice(idx, 1);
282
+ mockUsers.push({
283
+ ...user,
284
+ role: input.role,
285
+ private_metadata: { ...(user.private_metadata || {}), mako_api_key: apiKey }
286
+ });
287
+ return { apiKey };
288
+ }
289
+ /**
290
+ * Reject a pending user — permanently removes them from the identity provider.
291
+ * Matches UserManagementAdapter.rejectUser signature.
292
+ */
293
+ export async function rejectUser(userId) {
294
+ await delay();
295
+ const idx = mockPendingUsers.findIndex((u) => u.id === userId);
296
+ if (idx === -1) {
297
+ throw new Error(`Pending user ${userId} not found`);
298
+ }
299
+ mockPendingUsers.splice(idx, 1);
300
+ }
package/dist/index.d.ts CHANGED
@@ -60,7 +60,7 @@ export type { StepperProps, StepperStep, StepState, StepperOrientation } from '.
60
60
  export type { ActivityItemBadge, ActivityItemAction, ActivityItem, ActivityListProps, ActivityListSize } from './layout/activity-list/activity-list-types.js';
61
61
  export type { FileUploadProps, FileUploadSize, FilePreviewProps, UploadedFile, StagedFile } from './elements/file-upload/file-upload-types.js';
62
62
  export type { ChatMessageType, StreamingCallback, ChatAction, ChatMessage, ChatResponse, QuickAction, FileBrowserProps } from './ai/ai-types.js';
63
- export type { GetUsersOptions, GetUsersResult, UserEmail, UserPhone, User, Permission, Role, UserTableProps, UserModalProps, UserModalSavePayload, UserViewModalProps, UserManagementAdapter, UserManagementProps, FormErrors } from './user-management/user-management-types.js';
63
+ export type { GetUsersOptions, GetUsersResult, UserEmail, UserPhone, User, Permission, Role, UserTableProps, UserModalProps, UserModalSavePayload, UserViewModalProps, UserApproveModalProps, UserManagementAdapter, UserManagementProps, FormErrors } from './user-management/user-management-types.js';
64
64
  export { tv, cn } from './helper/cls.js';
65
65
  export { isRouteActive } from './helper/nav.svelte.js';
66
66
  export { default as Button } from './button/Button.svelte';
@@ -148,3 +148,7 @@ export { default as UserManagement } from './user-management/UserManagement.svel
148
148
  export { default as UserTable } from './user-management/UserTable.svelte';
149
149
  export { default as UserModal } from './user-management/UserModal.svelte';
150
150
  export { default as UserViewModal } from './user-management/UserViewModal.svelte';
151
+ export { default as UserApproveModal } from './user-management/UserApproveModal.svelte';
152
+ export { default as RoleCard } from './user-management/RoleCard.svelte';
153
+ export { default as ApiKeyField } from './user-management/ApiKeyField.svelte';
154
+ export { default as UserIdentityCard } from './user-management/UserIdentityCard.svelte';
package/dist/index.js CHANGED
@@ -153,3 +153,7 @@ export { default as UserManagement } from './user-management/UserManagement.svel
153
153
  export { default as UserTable } from './user-management/UserTable.svelte';
154
154
  export { default as UserModal } from './user-management/UserModal.svelte';
155
155
  export { default as UserViewModal } from './user-management/UserViewModal.svelte';
156
+ export { default as UserApproveModal } from './user-management/UserApproveModal.svelte';
157
+ export { default as RoleCard } from './user-management/RoleCard.svelte';
158
+ export { default as ApiKeyField } from './user-management/ApiKeyField.svelte';
159
+ export { default as UserIdentityCard } from './user-management/UserIdentityCard.svelte';
@@ -0,0 +1,165 @@
1
+ <script lang="ts">
2
+ import { Button, Color } from '../index.js';
3
+ import { cn } from '../helper/cls.js';
4
+ import type { ClassValue } from 'tailwind-variants';
5
+ import { toast } from 'svelte-sonner';
6
+
7
+ type ActionButton = {
8
+ label: string;
9
+ loading?: boolean;
10
+ disabled?: boolean;
11
+ onclick: () => void;
12
+ testId?: string;
13
+ };
14
+
15
+ interface Props {
16
+ /** The API key value. Empty string = no key issued yet. */
17
+ value: string;
18
+ label?: string;
19
+ /** Inline error message (e.g. regenerate failure). Suppresses helperText when set. */
20
+ error?: string | null;
21
+ /** Subtle hint shown below the field when no error. */
22
+ helperText?: string;
23
+ /** Action button rendered to the right of the label (e.g. Regenerate). */
24
+ regenerate?: ActionButton;
25
+ /** Optional second action (e.g. Verify). Rendered before regenerate. */
26
+ verify?: ActionButton;
27
+ /** Show the masked → revealed eye toggle. @default true */
28
+ toggleable?: boolean;
29
+ /** Show a Copy button next to the value. @default false */
30
+ copyable?: boolean;
31
+ /** When the key is missing, what to display. @default 'No API key' */
32
+ emptyLabel?: string;
33
+ testId?: string;
34
+ class?: ClassValue;
35
+ }
36
+
37
+ let {
38
+ value,
39
+ label = 'API Key',
40
+ error = null,
41
+ helperText,
42
+ regenerate,
43
+ verify,
44
+ toggleable = true,
45
+ copyable = false,
46
+ emptyLabel = 'No API key',
47
+ testId,
48
+ class: className
49
+ }: Props = $props();
50
+
51
+ let revealed = $state(false);
52
+ let copied = $state(false);
53
+
54
+ const masked = $derived(value ? '•'.repeat(Math.min(value.length, 40)) : '');
55
+ // If the field isn't toggleable, there's no way for the user to reveal it —
56
+ // so always show the raw value (used in one-time-reveal flows like approval).
57
+ const display = $derived(value ? (!toggleable || revealed ? value : masked) : emptyLabel);
58
+
59
+ async function handleCopy() {
60
+ if (!value) return;
61
+ try {
62
+ await navigator.clipboard.writeText(value);
63
+ copied = true;
64
+ setTimeout(() => (copied = false), 2000);
65
+ } catch {
66
+ toast.error('Failed to copy API key');
67
+ }
68
+ }
69
+ </script>
70
+
71
+ <div class={cn('w-full', className)}>
72
+ {#if label || regenerate || verify}
73
+ <div class="mb-2 flex items-center justify-between gap-3">
74
+ {#if label}
75
+ <span class="text-default-700 text-sm font-medium">{label}</span>
76
+ {/if}
77
+ {#if verify || regenerate}
78
+ <div class="flex items-center gap-2">
79
+ {#if verify}
80
+ <Button
81
+ type="button"
82
+ size="sm"
83
+ variant="link"
84
+ color={Color.SUCCESS}
85
+ onclick={verify.onclick}
86
+ disabled={verify.disabled || verify.loading}
87
+ loading={verify.loading}
88
+ testId={verify.testId}
89
+ >
90
+ {verify.label}
91
+ </Button>
92
+ {/if}
93
+ {#if regenerate}
94
+ <Button
95
+ type="button"
96
+ size="sm"
97
+ variant="link"
98
+ color={Color.PRIMARY}
99
+ onclick={regenerate.onclick}
100
+ disabled={regenerate.disabled || regenerate.loading}
101
+ loading={regenerate.loading}
102
+ testId={regenerate.testId}
103
+ >
104
+ {regenerate.label}
105
+ </Button>
106
+ {/if}
107
+ </div>
108
+ {/if}
109
+ </div>
110
+ {/if}
111
+
112
+ <div class="border-default-300 bg-default-50 flex items-center gap-2 rounded-lg border px-3 py-2">
113
+ <code
114
+ class="text-default-900 min-w-0 flex-1 font-mono text-sm break-all"
115
+ data-testid={testId ? `${testId}-value` : undefined}
116
+ >
117
+ {display}
118
+ </code>
119
+ {#if value && toggleable}
120
+ <button
121
+ type="button"
122
+ onclick={() => (revealed = !revealed)}
123
+ class="text-default-500 hover:text-default-700 shrink-0 cursor-pointer"
124
+ aria-label={revealed ? 'Hide API key' : 'Show API key'}
125
+ >
126
+ {#if revealed}
127
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
128
+ <path
129
+ stroke-linecap="round"
130
+ stroke-linejoin="round"
131
+ stroke-width="2"
132
+ d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.29 3.29m0 0A9.966 9.966 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
133
+ ></path>
134
+ </svg>
135
+ {:else}
136
+ <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
+ <path
138
+ stroke-linecap="round"
139
+ stroke-linejoin="round"
140
+ stroke-width="2"
141
+ d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
142
+ ></path>
143
+ <path
144
+ stroke-linecap="round"
145
+ stroke-linejoin="round"
146
+ stroke-width="2"
147
+ d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
148
+ ></path>
149
+ </svg>
150
+ {/if}
151
+ </button>
152
+ {/if}
153
+ {#if value && copyable}
154
+ <Button type="button" size="sm" variant="outline" onclick={handleCopy}>
155
+ {copied ? 'Copied' : 'Copy'}
156
+ </Button>
157
+ {/if}
158
+ </div>
159
+
160
+ {#if error}
161
+ <p class="text-danger-500 mt-1 text-xs">{error}</p>
162
+ {:else if helperText}
163
+ <p class="text-default-500 mt-1 text-xs">{helperText}</p>
164
+ {/if}
165
+ </div>
@@ -0,0 +1,32 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ type ActionButton = {
3
+ label: string;
4
+ loading?: boolean;
5
+ disabled?: boolean;
6
+ onclick: () => void;
7
+ testId?: string;
8
+ };
9
+ interface Props {
10
+ /** The API key value. Empty string = no key issued yet. */
11
+ value: string;
12
+ label?: string;
13
+ /** Inline error message (e.g. regenerate failure). Suppresses helperText when set. */
14
+ error?: string | null;
15
+ /** Subtle hint shown below the field when no error. */
16
+ helperText?: string;
17
+ /** Action button rendered to the right of the label (e.g. Regenerate). */
18
+ regenerate?: ActionButton;
19
+ /** Optional second action (e.g. Verify). Rendered before regenerate. */
20
+ verify?: ActionButton;
21
+ /** Show the masked → revealed eye toggle. @default true */
22
+ toggleable?: boolean;
23
+ /** Show a Copy button next to the value. @default false */
24
+ copyable?: boolean;
25
+ /** When the key is missing, what to display. @default 'No API key' */
26
+ emptyLabel?: string;
27
+ testId?: string;
28
+ class?: ClassValue;
29
+ }
30
+ declare const ApiKeyField: import("svelte").Component<Props, {}, "">;
31
+ type ApiKeyField = ReturnType<typeof ApiKeyField>;
32
+ export default ApiKeyField;
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import { cn } from '../helper/cls.js';
3
+ import type { ClassValue } from 'tailwind-variants';
4
+ import type { Role } from '../index.js';
5
+
6
+ interface Props {
7
+ role: Role;
8
+ selected?: boolean;
9
+ /** When false, renders as a static card (no button, no hover). @default true */
10
+ interactive?: boolean;
11
+ /** Reduces opacity to signal "preserved selection" (e.g. admin role on edit). */
12
+ dimmed?: boolean;
13
+ onclick?: () => void;
14
+ testId?: string;
15
+ class?: ClassValue;
16
+ }
17
+
18
+ let {
19
+ role,
20
+ selected = false,
21
+ interactive = true,
22
+ dimmed = false,
23
+ onclick,
24
+ testId,
25
+ class: className
26
+ }: Props = $props();
27
+
28
+ const baseClass = 'rounded-lg border-2 p-2 text-left transition-all w-full block';
29
+ const selectedClass = 'border-blue-500 bg-blue-50';
30
+ const idleClass = 'border-default-200 bg-white';
31
+ const interactiveIdle = 'hover:border-default-300 cursor-pointer';
32
+
33
+ const composed = $derived(
34
+ cn(
35
+ baseClass,
36
+ selected ? selectedClass : idleClass,
37
+ interactive && !selected && interactiveIdle,
38
+ dimmed && 'opacity-75',
39
+ className
40
+ )
41
+ );
42
+ </script>
43
+
44
+ {#snippet content()}
45
+ <div class="flex items-center justify-between gap-2">
46
+ <div class="min-w-0 flex-1">
47
+ <h4 class="text-default-900 text-sm font-semibold">{role.label}</h4>
48
+ {#if role.description}
49
+ <p class="text-default-600 mt-1 line-clamp-2 text-xs">{role.description}</p>
50
+ {/if}
51
+ </div>
52
+ <div
53
+ class={cn(
54
+ 'flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2',
55
+ selected ? 'border-blue-500 bg-blue-500' : 'border-default-300 bg-white'
56
+ )}
57
+ >
58
+ {#if selected}
59
+ <div class="h-2 w-2 rounded-full bg-white"></div>
60
+ {/if}
61
+ </div>
62
+ </div>
63
+ {/snippet}
64
+
65
+ {#if interactive}
66
+ <button type="button" {onclick} class={composed} data-testid={testId ?? `role-${role.value}`}>
67
+ {@render content()}
68
+ </button>
69
+ {:else}
70
+ <div class={composed} data-testid={testId ?? `role-${role.value}`}>
71
+ {@render content()}
72
+ </div>
73
+ {/if}
@@ -0,0 +1,16 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Role } from '../index.js';
3
+ interface Props {
4
+ role: Role;
5
+ selected?: boolean;
6
+ /** When false, renders as a static card (no button, no hover). @default true */
7
+ interactive?: boolean;
8
+ /** Reduces opacity to signal "preserved selection" (e.g. admin role on edit). */
9
+ dimmed?: boolean;
10
+ onclick?: () => void;
11
+ testId?: string;
12
+ class?: ClassValue;
13
+ }
14
+ declare const RoleCard: import("svelte").Component<Props, {}, "">;
15
+ type RoleCard = ReturnType<typeof RoleCard>;
16
+ export default RoleCard;
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ import { Modal, Button, Color, getUserDisplayName } from '../index.js';
3
+ import RoleCard from './RoleCard.svelte';
4
+ import ApiKeyField from './ApiKeyField.svelte';
5
+ import UserIdentityCard from './UserIdentityCard.svelte';
6
+ import type { UserApproveModalProps } from '../index.js';
7
+ import { toast } from 'svelte-sonner';
8
+
9
+ let {
10
+ open = $bindable(false),
11
+ user,
12
+ roles = [],
13
+ onapprove,
14
+ onclose
15
+ }: UserApproveModalProps = $props();
16
+
17
+ let selectedRole = $state<string>('');
18
+ let approving = $state(false);
19
+ let issuedKey = $state<string | null>(null);
20
+
21
+ $effect(() => {
22
+ if (!open) {
23
+ selectedRole = '';
24
+ approving = false;
25
+ issuedKey = null;
26
+ }
27
+ });
28
+
29
+ const canApprove = $derived(!!selectedRole && !approving && !!user);
30
+
31
+ async function handleApprove() {
32
+ if (!user || !selectedRole) return;
33
+ approving = true;
34
+ try {
35
+ const result = await onapprove({ userId: user.id, role: selectedRole });
36
+ issuedKey = result.apiKey;
37
+ } catch (error) {
38
+ toast.error(error instanceof Error ? error.message : 'Failed to approve user');
39
+ } finally {
40
+ approving = false;
41
+ }
42
+ }
43
+
44
+ function handleDone() {
45
+ open = false;
46
+ onclose();
47
+ }
48
+ </script>
49
+
50
+ <Modal
51
+ bind:open
52
+ title={issuedKey ? 'API key generated' : 'Approve user'}
53
+ description={issuedKey
54
+ ? 'Copy this key now — for security it will not be shown again.'
55
+ : user
56
+ ? `Assign a role to ${getUserDisplayName(user)} and generate their first API key.`
57
+ : undefined}
58
+ size="md"
59
+ footerAlign="end"
60
+ onclose={handleDone}
61
+ >
62
+ {#if !issuedKey}
63
+ <div class="flex flex-col gap-4">
64
+ <UserIdentityCard {user} />
65
+
66
+ {#if roles.length > 0}
67
+ <div>
68
+ <span class="text-default-700 mb-2 block text-sm font-medium">
69
+ Role <span class="text-danger-500">*</span>
70
+ </span>
71
+ <div class="grid grid-cols-1 gap-2">
72
+ {#each roles as role, index (`${role.value}-${index}`)}
73
+ <RoleCard
74
+ {role}
75
+ selected={selectedRole === role.value}
76
+ onclick={() => (selectedRole = role.value)}
77
+ testId="approve-role-{role.value}"
78
+ />
79
+ {/each}
80
+ </div>
81
+ </div>
82
+ {:else}
83
+ <p class="text-danger-600 text-xs">
84
+ No roles configured — pass a `roles` prop to enable approval.
85
+ </p>
86
+ {/if}
87
+ </div>
88
+ {:else}
89
+ <ApiKeyField
90
+ value={issuedKey}
91
+ label={undefined}
92
+ copyable
93
+ toggleable={false}
94
+ helperText="Store this key in your password manager or pass it to the user via a secure channel."
95
+ testId="approve-issued-key"
96
+ />
97
+ {/if}
98
+
99
+ {#snippet footer()}
100
+ {#if !issuedKey}
101
+ <Button variant="outline" onclick={handleDone}>Cancel</Button>
102
+ <Button
103
+ color={Color.PRIMARY}
104
+ onclick={handleApprove}
105
+ disabled={!canApprove}
106
+ loading={approving}
107
+ testId="approve-confirm"
108
+ >
109
+ Approve & Generate Key
110
+ </Button>
111
+ {:else}
112
+ <Button color={Color.PRIMARY} onclick={handleDone} testId="approve-done">Done</Button>
113
+ {/if}
114
+ {/snippet}
115
+ </Modal>
@@ -0,0 +1,4 @@
1
+ import type { UserApproveModalProps } from '../index.js';
2
+ declare const UserApproveModal: import("svelte").Component<UserApproveModalProps, {}, "open">;
3
+ type UserApproveModal = ReturnType<typeof UserApproveModal>;
4
+ export default UserApproveModal;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import { cn } from '../helper/cls.js';
3
+ import { getUserDisplayName, getUserInitials } from './user-management.js';
4
+ import type { ClassValue } from 'tailwind-variants';
5
+ import type { User } from '../index.js';
6
+
7
+ interface Props {
8
+ user: User | null;
9
+ /** Optional chip rendered on the right (e.g. current role label). */
10
+ roleLabel?: string;
11
+ class?: ClassValue;
12
+ }
13
+
14
+ let { user, roleLabel, class: className }: Props = $props();
15
+ </script>
16
+
17
+ {#if user}
18
+ <div
19
+ class={cn(
20
+ 'bg-default-50 border-default-200 flex items-center gap-3 rounded-lg border p-3',
21
+ className
22
+ )}
23
+ >
24
+ <div class="h-10 w-10 shrink-0">
25
+ {#if user.image_url}
26
+ <img class="h-10 w-10 rounded-full" src={user.image_url} alt="" />
27
+ {:else}
28
+ <div class="bg-default-300 flex h-10 w-10 items-center justify-center rounded-full">
29
+ <span class="text-default-700 text-sm font-medium">
30
+ {getUserInitials(user)}
31
+ </span>
32
+ </div>
33
+ {/if}
34
+ </div>
35
+ <div class="min-w-0 flex-1">
36
+ <div class="text-default-900 truncate text-sm font-medium">
37
+ {getUserDisplayName(user)}
38
+ </div>
39
+ {#if user.email_addresses?.[0]}
40
+ <div class="text-default-500 truncate text-xs">
41
+ {user.email_addresses[0].email_address}
42
+ </div>
43
+ {/if}
44
+ </div>
45
+ {#if roleLabel}
46
+ <span
47
+ class="border-primary-200 bg-primary-50 text-primary-700 shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium"
48
+ >
49
+ {roleLabel}
50
+ </span>
51
+ {/if}
52
+ </div>
53
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { User } from '../index.js';
3
+ interface Props {
4
+ user: User | null;
5
+ /** Optional chip rendered on the right (e.g. current role label). */
6
+ roleLabel?: string;
7
+ class?: ClassValue;
8
+ }
9
+ declare const UserIdentityCard: import("svelte").Component<Props, {}, "">;
10
+ type UserIdentityCard = ReturnType<typeof UserIdentityCard>;
11
+ export default UserIdentityCard;