@makolabs/ripple 3.0.9 → 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.
- package/dist/drawer/Drawer.svelte +33 -12
- package/dist/drawer/drawer-types.d.ts +13 -5
- package/dist/drawer/drawer.d.ts +45 -0
- package/dist/drawer/drawer.js +14 -5
- package/dist/funcs/mock-user-management.d.ts +40 -0
- package/dist/funcs/mock-user-management.js +85 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/modal/ConfirmDialog.svelte +6 -9
- package/dist/modal/Modal.svelte +18 -14
- package/dist/modal/modal-types.d.ts +9 -5
- package/dist/modal/modal.d.ts +42 -0
- package/dist/modal/modal.js +18 -8
- package/dist/user-management/ApiKeyField.svelte +165 -0
- package/dist/user-management/ApiKeyField.svelte.d.ts +32 -0
- package/dist/user-management/RoleCard.svelte +73 -0
- package/dist/user-management/RoleCard.svelte.d.ts +16 -0
- package/dist/user-management/UserApproveModal.svelte +115 -0
- package/dist/user-management/UserApproveModal.svelte.d.ts +4 -0
- package/dist/user-management/UserIdentityCard.svelte +53 -0
- package/dist/user-management/UserIdentityCard.svelte.d.ts +11 -0
- package/dist/user-management/UserManagement.svelte +191 -20
- package/dist/user-management/UserModal.svelte +203 -439
- package/dist/user-management/UserTable.svelte +48 -55
- package/dist/user-management/UserViewModal.svelte +87 -221
- package/dist/user-management/UserViewModal.svelte.d.ts +1 -1
- package/dist/user-management/user-management-types.d.ts +52 -3
- package/package.json +1 -1
- package/dist/modal/ModalFooter.svelte +0 -35
- package/dist/modal/ModalFooter.svelte.d.ts +0 -11
|
@@ -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,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;
|