@svadmin/ui 0.45.0 → 0.47.1
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/components/AboutSettings.svelte +21 -96
- package/dist/components/ApiSettings.svelte +54 -234
- package/dist/components/AppearanceSettings.svelte +36 -165
- package/dist/components/AuthPageShell.svelte +32 -0
- package/dist/components/AuthPageShell.svelte.d.ts +11 -0
- package/dist/components/AutoForm.svelte +21 -3
- package/dist/components/DetailDrawer.svelte +74 -0
- package/dist/components/DetailDrawer.svelte.d.ts +16 -0
- package/dist/components/FieldRenderer.svelte +70 -2
- package/dist/components/FieldRenderer.svelte.d.ts +2 -0
- package/dist/components/ForgotPasswordPage.svelte +4 -68
- package/dist/components/IntegrationsSettings.svelte +22 -61
- package/dist/components/LoginPage.svelte +33 -124
- package/dist/components/NotificationsSettings.svelte +22 -103
- package/dist/components/PermissionMatrix.svelte +18 -9
- package/dist/components/RegisterPage.svelte +7 -62
- package/dist/components/RowActions.svelte +154 -0
- package/dist/components/RowActions.svelte.d.ts +26 -0
- package/dist/components/SecuritySettings.svelte +18 -58
- package/dist/components/SettingsPage.svelte +12 -2
- package/dist/components/SvadminLogo.svelte +1 -1
- package/dist/components/UpdatePasswordPage.svelte +3 -55
- package/dist/components/account/CompanyProfilePage.svelte +19 -7
- package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
- package/dist/components/account/SettingsPlainPage.svelte +25 -3
- package/dist/components/account/SettingsSidebarPage.svelte +31 -2
- package/dist/components/account/UserProfilePage.svelte +5 -4
- package/dist/components/content/ApiKeyList.svelte +40 -6
- package/dist/components/content/ApiKeyList.svelte.d.ts +10 -0
- package/dist/components/content/DataState.svelte +25 -9
- package/dist/components/content/DataState.svelte.d.ts +2 -0
- package/dist/components/content/FileList.svelte +12 -4
- package/dist/components/content/FileList.svelte.d.ts +9 -0
- package/dist/components/content/FilterToolbar.svelte +10 -2
- package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
- package/dist/components/content/MemberList.svelte +13 -5
- package/dist/components/content/MemberList.svelte.d.ts +9 -0
- package/dist/components/content/MetricBlock.svelte +19 -2
- package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
- package/dist/components/content/NetworkTable.svelte +9 -3
- package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
- package/dist/components/content/NetworkUserCard.svelte +47 -7
- package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
- package/dist/components/content/SectionHeader.svelte +3 -2
- package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
- package/dist/components/content/SecurityEventTable.svelte +14 -5
- package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
- package/dist/components/content/SettingsFieldRow.svelte +28 -0
- package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
- package/dist/components/content/SettingsGroup.svelte +33 -0
- package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
- package/dist/components/content/WorkspaceLayout.svelte +31 -0
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +12 -0
- package/dist/components/content/index.d.ts +4 -1
- package/dist/components/content/index.js +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +25 -6
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -0
- package/package.json +4 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Ellipsis } from '@lucide/svelte';
|
|
3
|
+
import type { Component, Snippet } from 'svelte';
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
5
|
+
import { cn, type WithElementRef } from '../utils.js';
|
|
6
|
+
import * as DropdownMenu from './ui/dropdown-menu/index.js';
|
|
7
|
+
import { Button } from './ui/button/index.js';
|
|
8
|
+
|
|
9
|
+
export type RowActionItem = {
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: Component<{ class?: string }>;
|
|
12
|
+
href?: string;
|
|
13
|
+
onclick?: (e: MouseEvent) => void;
|
|
14
|
+
variant?: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
title?: string;
|
|
17
|
+
danger?: boolean;
|
|
18
|
+
hidden?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
22
|
+
actions?: RowActionItem[];
|
|
23
|
+
maxVisible?: number;
|
|
24
|
+
moreLabel?: string;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
moreContent?: Snippet;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
actions = [],
|
|
31
|
+
maxVisible = 2,
|
|
32
|
+
moreLabel = 'More actions',
|
|
33
|
+
ref = $bindable(null),
|
|
34
|
+
class: className,
|
|
35
|
+
children,
|
|
36
|
+
moreContent,
|
|
37
|
+
...restProps
|
|
38
|
+
}: Props = $props();
|
|
39
|
+
|
|
40
|
+
let overflowOpen = $state(false);
|
|
41
|
+
const availableActions = $derived(actions.filter((action) => !action.hidden));
|
|
42
|
+
const visibleLimit = $derived(
|
|
43
|
+
Number.isFinite(maxVisible)
|
|
44
|
+
? Math.max(0, Math.floor(maxVisible))
|
|
45
|
+
: availableActions.length
|
|
46
|
+
);
|
|
47
|
+
const visibleActions = $derived(availableActions.slice(0, visibleLimit));
|
|
48
|
+
const overflowActions = $derived(availableActions.slice(visibleLimit));
|
|
49
|
+
|
|
50
|
+
function resolvedVariant(action: RowActionItem): RowActionItem['variant'] {
|
|
51
|
+
return action.variant ?? (action.danger ? 'destructive' : 'ghost');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleOverflowAction(action: RowActionItem, event: MouseEvent): void {
|
|
55
|
+
if (action.disabled) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
overflowOpen = false;
|
|
60
|
+
action.onclick?.(event);
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div
|
|
65
|
+
bind:this={ref}
|
|
66
|
+
data-slot="row-actions"
|
|
67
|
+
class={cn("inline-flex items-center gap-1.5", className)}
|
|
68
|
+
{...restProps}
|
|
69
|
+
>
|
|
70
|
+
{#if children}
|
|
71
|
+
{@render children()}
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
{#each visibleActions as action (action)}
|
|
75
|
+
{#if action.href}
|
|
76
|
+
<Button
|
|
77
|
+
variant={resolvedVariant(action)}
|
|
78
|
+
size="sm"
|
|
79
|
+
href={action.href}
|
|
80
|
+
disabled={action.disabled}
|
|
81
|
+
title={action.title || action.label}
|
|
82
|
+
class="h-8 px-2 text-xs"
|
|
83
|
+
>
|
|
84
|
+
{#if action.icon}
|
|
85
|
+
{@const Icon = action.icon}
|
|
86
|
+
<Icon class="mr-1 size-3.5" />
|
|
87
|
+
{/if}
|
|
88
|
+
{action.label}
|
|
89
|
+
</Button>
|
|
90
|
+
{:else}
|
|
91
|
+
<Button
|
|
92
|
+
variant={resolvedVariant(action)}
|
|
93
|
+
size="sm"
|
|
94
|
+
onclick={action.onclick}
|
|
95
|
+
disabled={action.disabled}
|
|
96
|
+
title={action.title || action.label}
|
|
97
|
+
class="h-8 px-2 text-xs"
|
|
98
|
+
>
|
|
99
|
+
{#if action.icon}
|
|
100
|
+
{@const Icon = action.icon}
|
|
101
|
+
<Icon class="mr-1 size-3.5" />
|
|
102
|
+
{/if}
|
|
103
|
+
{action.label}
|
|
104
|
+
</Button>
|
|
105
|
+
{/if}
|
|
106
|
+
{/each}
|
|
107
|
+
|
|
108
|
+
{#if overflowActions.length > 0 || moreContent}
|
|
109
|
+
<DropdownMenu.Root bind:open={overflowOpen}>
|
|
110
|
+
<DropdownMenu.Trigger>
|
|
111
|
+
{#snippet child({ props })}
|
|
112
|
+
<Button {...props} variant="ghost" size="icon-sm" title={moreLabel} aria-label={moreLabel}>
|
|
113
|
+
<Ellipsis class="size-4" />
|
|
114
|
+
</Button>
|
|
115
|
+
{/snippet}
|
|
116
|
+
</DropdownMenu.Trigger>
|
|
117
|
+
<DropdownMenu.Content align="end" class="min-w-[8rem]">
|
|
118
|
+
{#each overflowActions as action (action)}
|
|
119
|
+
{#if action.href}
|
|
120
|
+
<Button
|
|
121
|
+
href={action.href}
|
|
122
|
+
variant={resolvedVariant(action)}
|
|
123
|
+
size="sm"
|
|
124
|
+
disabled={action.disabled}
|
|
125
|
+
onclick={(event) => handleOverflowAction(action, event)}
|
|
126
|
+
class="h-auto w-full justify-start rounded-md px-2 py-1.5 text-sm font-normal"
|
|
127
|
+
>
|
|
128
|
+
{#if action.icon}
|
|
129
|
+
{@const Icon = action.icon}
|
|
130
|
+
<Icon class="size-3.5" />
|
|
131
|
+
{/if}
|
|
132
|
+
{action.label}
|
|
133
|
+
</Button>
|
|
134
|
+
{:else}
|
|
135
|
+
<DropdownMenu.Item
|
|
136
|
+
onclick={(event) => handleOverflowAction(action, event)}
|
|
137
|
+
disabled={action.disabled}
|
|
138
|
+
destructive={action.danger || action.variant === 'destructive'}
|
|
139
|
+
>
|
|
140
|
+
{#if action.icon}
|
|
141
|
+
{@const Icon = action.icon}
|
|
142
|
+
<Icon class="size-3.5" />
|
|
143
|
+
{/if}
|
|
144
|
+
{action.label}
|
|
145
|
+
</DropdownMenu.Item>
|
|
146
|
+
{/if}
|
|
147
|
+
{/each}
|
|
148
|
+
{#if moreContent}
|
|
149
|
+
{@render moreContent()}
|
|
150
|
+
{/if}
|
|
151
|
+
</DropdownMenu.Content>
|
|
152
|
+
</DropdownMenu.Root>
|
|
153
|
+
{/if}
|
|
154
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type WithElementRef } from '../utils.js';
|
|
4
|
+
export type RowActionItem = {
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: Component<{
|
|
7
|
+
class?: string;
|
|
8
|
+
}>;
|
|
9
|
+
href?: string;
|
|
10
|
+
onclick?: (e: MouseEvent) => void;
|
|
11
|
+
variant?: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
title?: string;
|
|
14
|
+
danger?: boolean;
|
|
15
|
+
hidden?: boolean;
|
|
16
|
+
};
|
|
17
|
+
type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
|
18
|
+
actions?: RowActionItem[];
|
|
19
|
+
maxVisible?: number;
|
|
20
|
+
moreLabel?: string;
|
|
21
|
+
children?: Snippet;
|
|
22
|
+
moreContent?: Snippet;
|
|
23
|
+
};
|
|
24
|
+
declare const RowActions: Component<Props, {}, "ref">;
|
|
25
|
+
type RowActions = ReturnType<typeof RowActions>;
|
|
26
|
+
export default RowActions;
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { useNotification } from '@svadmin/core';
|
|
3
3
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
4
4
|
|
|
5
|
-
import { AlertTriangle,
|
|
5
|
+
import { AlertTriangle, Monitor, Smartphone, Trash2 } from '@lucide/svelte';
|
|
6
6
|
import type { Component } from 'svelte';
|
|
7
7
|
import * as Alert from './ui/alert/index.js';
|
|
8
|
-
import * as Card from './ui/card/index.js';
|
|
9
8
|
import { Button } from './ui/button/index.js';
|
|
10
|
-
import { Label } from './ui/label/index.js';
|
|
11
9
|
import { Switch } from './ui/switch/index.js';
|
|
12
10
|
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
import SettingsGroup from './content/SettingsGroup.svelte';
|
|
12
|
+
import SettingsFieldRow from './content/SettingsFieldRow.svelte';
|
|
13
|
+
import DataState from './content/DataState.svelte';
|
|
13
14
|
|
|
14
15
|
const i18n = useTranslation();
|
|
15
16
|
const notification = useNotification();
|
|
@@ -76,41 +77,14 @@
|
|
|
76
77
|
<p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.securityDescription')}</p>
|
|
77
78
|
</div>
|
|
78
79
|
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{i18n.t('security.twoFactorAuth')}
|
|
88
|
-
</Card.CardTitle>
|
|
89
|
-
<Card.CardDescription>{i18n.t('security.twoFactorDescription')}</Card.CardDescription>
|
|
90
|
-
</Card.CardHeader>
|
|
91
|
-
<Card.CardContent>
|
|
92
|
-
<div class="flex items-center justify-between gap-4 border-t pt-4">
|
|
93
|
-
<div class="space-y-1">
|
|
94
|
-
<Label for="two-factor" class="text-sm font-medium">{i18n.t('security.enable2fa')}</Label>
|
|
95
|
-
<p class="text-xs text-muted-foreground">
|
|
96
|
-
{is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}
|
|
97
|
-
</p>
|
|
98
|
-
</div>
|
|
99
|
-
<Switch id="two-factor" bind:checked={is2faEnabled} />
|
|
100
|
-
</div>
|
|
101
|
-
</Card.CardContent>
|
|
102
|
-
</Card.Card>
|
|
103
|
-
|
|
104
|
-
<Card.Card>
|
|
105
|
-
<Card.CardHeader>
|
|
106
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
107
|
-
<KeyRound class="h-4 w-4 text-muted-foreground" />
|
|
108
|
-
{i18n.t('profile.changePassword')}
|
|
109
|
-
</Card.CardTitle>
|
|
110
|
-
<Card.CardDescription>{i18n.t('security.passwordLastChanged', { time: lastChanged })}</Card.CardDescription>
|
|
111
|
-
</Card.CardHeader>
|
|
112
|
-
<Card.CardContent>
|
|
113
|
-
<form onsubmit={handlePasswordChange} class="max-w-sm space-y-4">
|
|
80
|
+
<SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')}>
|
|
81
|
+
<SettingsFieldRow label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
|
|
82
|
+
{#snippet control()}<Switch id="two-factor" bind:checked={is2faEnabled} />{/snippet}
|
|
83
|
+
</SettingsFieldRow>
|
|
84
|
+
</SettingsGroup>
|
|
85
|
+
|
|
86
|
+
<SettingsGroup title={i18n.t('profile.changePassword')} description={i18n.t('security.passwordLastChanged', { time: lastChanged })}>
|
|
87
|
+
<form onsubmit={handlePasswordChange} class="max-w-lg space-y-4">
|
|
114
88
|
{#if passwordErrorMessage}
|
|
115
89
|
<Alert.Root variant="destructive">
|
|
116
90
|
<AlertTriangle class="h-4 w-4" />
|
|
@@ -124,23 +98,10 @@
|
|
|
124
98
|
|
|
125
99
|
<Button type="submit">{i18n.t('profile.updatePassword')}</Button>
|
|
126
100
|
</form>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<Card.CardHeader class="flex flex-row items-start justify-between gap-4">
|
|
132
|
-
<div class="space-y-1">
|
|
133
|
-
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
134
|
-
<Shield class="h-4 w-4 text-muted-foreground" />
|
|
135
|
-
{i18n.t('security.activeSessions')}
|
|
136
|
-
</Card.CardTitle>
|
|
137
|
-
<Card.CardDescription>{i18n.t('security.sessionsDescription')}</Card.CardDescription>
|
|
138
|
-
</div>
|
|
139
|
-
{#if sessions.length > 1}
|
|
140
|
-
<Button variant="outline" size="sm" onclick={revokeAllOthers}>{i18n.t('security.revokeOthers')}</Button>
|
|
141
|
-
{/if}
|
|
142
|
-
</Card.CardHeader>
|
|
143
|
-
<Card.CardContent>
|
|
101
|
+
</SettingsGroup>
|
|
102
|
+
|
|
103
|
+
<SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')}>
|
|
104
|
+
{#snippet actions()}{#if sessions.length > 1}<Button variant="outline" size="sm" onclick={revokeAllOthers}>{i18n.t('security.revokeOthers')}</Button>{/if}{/snippet}
|
|
144
105
|
<div class="divide-y">
|
|
145
106
|
{#each sessions as session (session.id)}
|
|
146
107
|
<div class="flex items-center justify-between gap-4 py-3">
|
|
@@ -168,9 +129,8 @@
|
|
|
168
129
|
{/if}
|
|
169
130
|
</div>
|
|
170
131
|
{:else}
|
|
171
|
-
<
|
|
132
|
+
<DataState state="empty" title={i18n.t('security.noSessions')} />
|
|
172
133
|
{/each}
|
|
173
134
|
</div>
|
|
174
|
-
|
|
175
|
-
</Card.Card>
|
|
135
|
+
</SettingsGroup>
|
|
176
136
|
</div>
|
|
@@ -115,6 +115,16 @@
|
|
|
115
115
|
return content ? alias : "profile";
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
const contentWidth = $derived(
|
|
119
|
+
activeKey === 'roles' || activeKey === 'audit'
|
|
120
|
+
? 'max-w-[92rem]'
|
|
121
|
+
: activeKey === 'api'
|
|
122
|
+
? 'max-w-[78rem]'
|
|
123
|
+
: activeKey === 'integrations'
|
|
124
|
+
? 'max-w-[64rem]'
|
|
125
|
+
: 'max-w-3xl',
|
|
126
|
+
);
|
|
127
|
+
|
|
118
128
|
// Default redirect to profile
|
|
119
129
|
$effect(() => {
|
|
120
130
|
if (getRoute() === "/settings") {
|
|
@@ -151,7 +161,7 @@
|
|
|
151
161
|
</div>
|
|
152
162
|
{#each resolvedSections as section, _i (_i)}
|
|
153
163
|
<div>
|
|
154
|
-
<h3 class="px-3 mb-1 text-
|
|
164
|
+
<h3 class="px-3 mb-1 text-xs font-medium text-muted-foreground">
|
|
155
165
|
{i18n.t(section.group)}
|
|
156
166
|
</h3>
|
|
157
167
|
<div class="space-y-0.5">
|
|
@@ -175,7 +185,7 @@
|
|
|
175
185
|
</nav>
|
|
176
186
|
|
|
177
187
|
<!-- Right content area -->
|
|
178
|
-
<div class="flex-1 p-6 lg:p-8 {
|
|
188
|
+
<div class="min-w-0 w-full flex-1 p-6 lg:p-8 {contentWidth}">
|
|
179
189
|
{#if activeKey === "profile"}
|
|
180
190
|
{#if profile}{@render profile()}{:else}<ProfilePage />{/if}
|
|
181
191
|
{:else if activeKey === "appearance"}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<span
|
|
12
12
|
aria-hidden="true"
|
|
13
|
-
class="inline-block shrink-0 rounded-lg bg-
|
|
13
|
+
class="inline-block shrink-0 rounded-lg bg-primary shadow-sm {onDark ? 'ring-1 ring-white/35' : ''}"
|
|
14
14
|
style:width={`${size}px`}
|
|
15
15
|
style:height={`${size}px`}
|
|
16
16
|
></span>
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import * as Alert from './ui/alert/index.js';
|
|
7
|
+
import AuthPageShell from './AuthPageShell.svelte';
|
|
7
8
|
import PasswordInput from './PasswordInput.svelte';
|
|
8
|
-
import SvadminLogo from './SvadminLogo.svelte';
|
|
9
9
|
import { Loader2, AlertCircle } from '@lucide/svelte';
|
|
10
10
|
|
|
11
11
|
const i18n = useTranslation();
|
|
@@ -35,53 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
</script>
|
|
37
37
|
|
|
38
|
-
<
|
|
39
|
-
<!-- Left brand panel -->
|
|
40
|
-
<div class="hidden lg:flex lg:w-[480px] xl:w-[520px] relative overflow-hidden bg-gradient-to-br from-primary/90 via-primary to-primary/80 shrink-0">
|
|
41
|
-
<div class="absolute inset-0">
|
|
42
|
-
<div class="absolute top-[12%] left-[8%] w-28 h-28 rounded-3xl border border-white/10 bg-white/5 rotate-12 backdrop-blur-sm"></div>
|
|
43
|
-
<div class="absolute top-[55%] left-[55%] w-40 h-40 rounded-full border border-white/10 bg-white/5 -rotate-6 backdrop-blur-sm"></div>
|
|
44
|
-
<div class="absolute bottom-[15%] left-[15%] w-24 h-24 rounded-2xl border border-white/10 bg-white/5 -rotate-12 backdrop-blur-sm"></div>
|
|
45
|
-
<div class="absolute top-[30%] right-[10%] w-16 h-16 rounded-xl border border-white/10 bg-white/5 rotate-45 backdrop-blur-sm"></div>
|
|
46
|
-
<div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
|
|
50
|
-
<div class="flex items-center gap-3">
|
|
51
|
-
<SvadminLogo size={36} onDark />
|
|
52
|
-
<span class="text-lg font-semibold text-white tracking-tight">{title}</span>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div class="space-y-4">
|
|
56
|
-
<h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
|
|
57
|
-
{i18n.t('auth.resetPassword')}
|
|
58
|
-
</h2>
|
|
59
|
-
<p class="text-base text-white/70 leading-relaxed max-w-[320px]">
|
|
60
|
-
{i18n.t('auth.resetPasswordDescription')}
|
|
61
|
-
</p>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<div class="flex items-center gap-3">
|
|
65
|
-
<div class="h-1 w-8 rounded-full bg-white/30"></div>
|
|
66
|
-
<div class="h-1 w-4 rounded-full bg-white/15"></div>
|
|
67
|
-
<div class="h-1 w-2 rounded-full bg-white/10"></div>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
<!-- Right form panel -->
|
|
73
|
-
<div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
|
|
74
|
-
<div class="w-full max-w-[400px]">
|
|
75
|
-
<div class="mb-8 flex items-center gap-3 lg:hidden">
|
|
76
|
-
<SvadminLogo size={36} />
|
|
77
|
-
<span class="text-lg font-semibold tracking-tight">{title}</span>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
<div class="space-y-1 mb-8">
|
|
81
|
-
<h1 class="text-2xl font-semibold tracking-tight">{i18n.t('auth.resetPassword')}</h1>
|
|
82
|
-
<p class="text-sm text-muted-foreground">{i18n.t('auth.resetPasswordDescription')}</p>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
38
|
+
<AuthPageShell brand={title} title={i18n.t('auth.resetPassword')} description={i18n.t('auth.resetPasswordDescription')}>
|
|
85
39
|
<form onsubmit={handleSubmit} class="space-y-5">
|
|
86
40
|
{#if error}
|
|
87
41
|
<Alert.Root variant="destructive">
|
|
@@ -118,10 +72,4 @@
|
|
|
118
72
|
</Button>
|
|
119
73
|
</div>
|
|
120
74
|
</form>
|
|
121
|
-
|
|
122
|
-
<p class="text-xs text-muted-foreground/60 mt-8 text-center">
|
|
123
|
-
Powered by {title}
|
|
124
|
-
</p>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
75
|
+
</AuthPageShell>
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
5
|
import { Input } from '../ui/input/index.js';
|
|
6
6
|
import { Label } from '../ui/label/index.js';
|
|
7
|
-
import
|
|
7
|
+
import { Textarea } from '../ui/textarea/index.js';
|
|
8
8
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
9
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
10
10
|
import DescriptionList from '../content/DescriptionList.svelte';
|
|
11
11
|
import MemberList from '../content/MemberList.svelte';
|
|
12
|
+
import SectionHeader from '../content/SectionHeader.svelte';
|
|
13
|
+
import SettingsGroup from '../content/SettingsGroup.svelte';
|
|
14
|
+
import WorkspaceLayout from '../content/WorkspaceLayout.svelte';
|
|
12
15
|
import type { MemberSummary } from '../content/MemberList.svelte';
|
|
13
16
|
const i18n = useTranslation();
|
|
14
17
|
let companyName = $state('Acme Corporation');
|
|
@@ -25,11 +28,20 @@
|
|
|
25
28
|
async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
|
|
26
29
|
</script>
|
|
27
30
|
|
|
31
|
+
{#snippet headerActions()}
|
|
32
|
+
<Button onclick={save} disabled={saving} size="sm">{#if saving}<Loader2 class="size-3.5 animate-spin" />{/if}{i18n.t('common.save')}</Button>
|
|
33
|
+
{/snippet}
|
|
34
|
+
|
|
35
|
+
{#snippet primary()}
|
|
36
|
+
<SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.companyProfileDescription')} bodyClass="space-y-4"><div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="company-name">{i18n.t('profile.companyName')}</Label><Input id="company-name" bind:value={companyName} /></div><div class="space-y-2"><Label for="company-industry">{i18n.t('profile.industry')}</Label><Input id="company-industry" bind:value={industry} /></div><div class="space-y-2 sm:col-span-2"><Label for="company-website">{i18n.t('profile.website')}</Label><Input id="company-website" bind:value={website} type="url" /></div></div><div class="space-y-2"><Label for="company-description">{i18n.t('profile.companyDescription')}</Label><Textarea id="company-description" bind:value={description} rows={4} /></div></SettingsGroup>
|
|
37
|
+
{/snippet}
|
|
38
|
+
|
|
39
|
+
{#snippet secondary()}
|
|
40
|
+
<SettingsGroup title={i18n.t('profile.companyName')} description={industry} bodyClass="space-y-4"><p class="text-lg font-semibold text-foreground">{companyName}</p><DescriptionList columns={1} items={[{ label: i18n.t('profile.website'), value: website, href: website }, { label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('profile.founded'), value: '2015' }]} /></SettingsGroup>
|
|
41
|
+
{/snippet}
|
|
42
|
+
|
|
28
43
|
<ContentPageShell pageId="account-company-profile" width="wide">
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
<Card.Card><Card.CardContent class="space-y-4 p-5"><div><p class="text-xs font-medium text-muted-foreground">{i18n.t('profile.companyName')}</p><p class="mt-1 text-lg font-semibold text-foreground">{companyName}</p><p class="text-sm text-muted-foreground">{industry}</p></div><DescriptionList columns={1} items={[{ label: i18n.t('profile.website'), value: website, href: website }, { label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('profile.founded'), value: '2015' }]} /></Card.CardContent></Card.Card>
|
|
33
|
-
</div>
|
|
34
|
-
<section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('profileSections.members')}</h2><MemberList {members} /></section>
|
|
44
|
+
<ContentPageHeader title={i18n.t('account.companyProfile')} description={i18n.t('account.companyProfileDescription')} actions={headerActions} />
|
|
45
|
+
<WorkspaceLayout {primary} {secondary} secondaryWidth="20rem" />
|
|
46
|
+
<section class="space-y-3"><SectionHeader title={i18n.t('profileSections.members')} /><MemberList {members} emptyTitle={i18n.t('common.noData')} emptyDescription={i18n.t('empty.description')} /></section>
|
|
35
47
|
</ContentPageShell>
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
import { Input } from '../ui/input/index.js';
|
|
5
5
|
import { Label } from '../ui/label/index.js';
|
|
6
6
|
import { Switch } from '../ui/switch/index.js';
|
|
7
|
-
import * as Card from '../ui/card/index.js';
|
|
8
7
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
8
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
|
+
import SectionHeader from '../content/SectionHeader.svelte';
|
|
10
|
+
import SettingsGroup from '../content/SettingsGroup.svelte';
|
|
11
|
+
import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
|
|
10
12
|
import IntegrationCard from '../content/IntegrationCard.svelte';
|
|
11
13
|
import type { IntegrationSummary } from '../content/IntegrationCard.svelte';
|
|
12
14
|
import SecurityEventTable from '../content/SecurityEventTable.svelte';
|
|
@@ -18,9 +20,28 @@
|
|
|
18
20
|
const events: SecurityEvent[] = [{ id: '1', event: 'SSO policy updated', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Today, 09:12', severity: 'info' }, { id: '2', event: 'Audit log retention changed', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Yesterday, 16:40', severity: 'warning' }];
|
|
19
21
|
</script>
|
|
20
22
|
|
|
23
|
+
{#snippet auditControl()}
|
|
24
|
+
<Switch bind:checked={auditLogging} aria-label={i18n.t('account.auditLogging')} />
|
|
25
|
+
{/snippet}
|
|
26
|
+
|
|
27
|
+
{#snippet ssoControl()}
|
|
28
|
+
<Switch bind:checked={ssoEnabled} aria-label={i18n.t('account.enableSso')} />
|
|
29
|
+
{/snippet}
|
|
30
|
+
|
|
21
31
|
<ContentPageShell pageId="account-settings-enterprise" width="wide">
|
|
22
32
|
<ContentPageHeader title={i18n.t('account.settingsEnterprise')} description={i18n.t('account.settingsEnterpriseDescription')} />
|
|
23
|
-
<div class="grid gap-4 lg:grid-cols-2"
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
<div class="grid items-start gap-4 lg:grid-cols-2">
|
|
34
|
+
<SettingsGroup title={i18n.t('account.securityPolicy')} description="Set organization-wide controls for authentication and sessions." bodyClass="space-y-4">
|
|
35
|
+
<div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label><Input id="session-timeout" value="30" /></div><div class="space-y-2"><Label for="retention">{i18n.t('account.dataRetention')}</Label><Input id="retention" value="365" /></div></div>
|
|
36
|
+
<SettingsFieldRow label={i18n.t('account.auditLogging')} description="Preserve an auditable record of organization-level changes." control={auditControl} separated />
|
|
37
|
+
<Button size="sm">{i18n.t('common.save')}</Button>
|
|
38
|
+
</SettingsGroup>
|
|
39
|
+
<SettingsGroup title={i18n.t('account.singleSignOn')} description={i18n.t('account.ssoDescription')} bodyClass="space-y-4">
|
|
40
|
+
<SettingsFieldRow label={i18n.t('account.enableSso')} description="Require the configured identity provider for organization members." control={ssoControl} />
|
|
41
|
+
<Input value="https://idp.example.com/metadata" aria-label={i18n.t('account.ssoProvider')} />
|
|
42
|
+
<Button variant="outline" size="sm">{i18n.t('common.test')}</Button>
|
|
43
|
+
</SettingsGroup>
|
|
44
|
+
</div>
|
|
45
|
+
<section class="space-y-3"><SectionHeader title={i18n.t('account.connections')} />{#each identity as integration (integration.id)}<IntegrationCard {integration} />{/each}</section>
|
|
46
|
+
<section class="space-y-3"><SectionHeader title={i18n.t('account.securityLog')} /><SecurityEventTable events={events} emptyTitle={i18n.t('common.noData')} emptyDescription={i18n.t('empty.description')} /></section>
|
|
26
47
|
</ContentPageShell>
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { Input } from '../ui/input/index.js';
|
|
5
5
|
import { Label } from '../ui/label/index.js';
|
|
6
6
|
import { Switch } from '../ui/switch/index.js';
|
|
7
|
-
import * as Card from '../ui/card/index.js';
|
|
8
7
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
9
8
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
9
|
+
import SettingsGroup from '../content/SettingsGroup.svelte';
|
|
10
|
+
import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
|
|
10
11
|
const i18n = useTranslation();
|
|
11
12
|
let displayName = $state('Alex Chen');
|
|
12
13
|
let email = $state('alex@example.com');
|
|
@@ -16,7 +17,28 @@
|
|
|
16
17
|
async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
|
|
17
18
|
</script>
|
|
18
19
|
|
|
20
|
+
{#snippet headerActions()}
|
|
21
|
+
<Button size="sm" onclick={save} disabled={saving}>{i18n.t('common.save')}</Button>
|
|
22
|
+
{/snippet}
|
|
23
|
+
|
|
24
|
+
{#snippet notificationControl()}
|
|
25
|
+
<Switch bind:checked={notifications} aria-label={i18n.t('settings.notifications')} />
|
|
26
|
+
{/snippet}
|
|
27
|
+
|
|
28
|
+
{#snippet twoFactorControl()}
|
|
29
|
+
<Switch bind:checked={twoFactor} aria-label={i18n.t('account.twoFactorAuth')} />
|
|
30
|
+
{/snippet}
|
|
31
|
+
|
|
19
32
|
<ContentPageShell pageId="account-settings-plain" width="narrow">
|
|
20
|
-
<
|
|
21
|
-
<
|
|
33
|
+
<ContentPageHeader title={i18n.t('account.settingsPlain')} description={i18n.t('account.settingsPlainDescription')} actions={headerActions} />
|
|
34
|
+
<SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.settingsPlainDescription')} bodyClass="space-y-5">
|
|
35
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
36
|
+
<div class="space-y-2"><Label for="settings-name">{i18n.t('profile.name')}</Label><Input id="settings-name" bind:value={displayName} /></div>
|
|
37
|
+
<div class="space-y-2"><Label for="settings-email">Email</Label><Input id="settings-email" bind:value={email} type="email" /></div>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<SettingsFieldRow label={i18n.t('settings.notifications')} description="Receive account and workflow updates." control={notificationControl} />
|
|
41
|
+
<SettingsFieldRow label={i18n.t('account.twoFactorAuth')} description="Require an additional verification code when signing in." control={twoFactorControl} separated />
|
|
42
|
+
</div>
|
|
43
|
+
</SettingsGroup>
|
|
22
44
|
</ContentPageShell>
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { Button } from '../ui/button/index.js';
|
|
4
4
|
import { Input } from '../ui/input/index.js';
|
|
5
5
|
import { Switch } from '../ui/switch/index.js';
|
|
6
|
-
import * as Card from '../ui/card/index.js';
|
|
7
6
|
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
8
7
|
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
8
|
+
import SettingsGroup from '../content/SettingsGroup.svelte';
|
|
9
|
+
import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
|
|
9
10
|
const i18n = useTranslation();
|
|
10
11
|
const sections = [{ id: 'profile', title: i18n.t('account.basicSettings') }, { id: 'signin', title: i18n.t('account.socialSignIn') }, { id: 'preferences', title: i18n.t('account.preferences') }, { id: 'api', title: i18n.t('account.manageApi') }];
|
|
11
12
|
let active = $state('profile');
|
|
@@ -13,7 +14,35 @@
|
|
|
13
14
|
let available = $state(true);
|
|
14
15
|
</script>
|
|
15
16
|
|
|
17
|
+
{#snippet visibilityControl()}
|
|
18
|
+
<Switch bind:checked={visible} aria-label={i18n.t('account.visibility')} />
|
|
19
|
+
{/snippet}
|
|
20
|
+
|
|
21
|
+
{#snippet availabilityControl()}
|
|
22
|
+
<Switch bind:checked={available} aria-label={i18n.t('account.availability')} />
|
|
23
|
+
{/snippet}
|
|
24
|
+
|
|
16
25
|
<ContentPageShell pageId="account-settings-sidebar" width="wide">
|
|
17
26
|
<ContentPageHeader title={i18n.t('account.settingsSidebar')} description={i18n.t('account.settingsSidebarDescription')} />
|
|
18
|
-
<div class="grid gap-6 lg:grid-cols-[12rem_minmax(0,1fr)]"
|
|
27
|
+
<div class="grid gap-6 lg:grid-cols-[12rem_minmax(0,1fr)]">
|
|
28
|
+
<nav class="flex gap-1 overflow-x-auto lg:flex-col" aria-label={i18n.t('account.settingsSidebar')}>
|
|
29
|
+
{#each sections as section (section.id)}
|
|
30
|
+
<button type="button" class={'whitespace-nowrap rounded-md px-3 py-2 text-left text-sm ' + (active === section.id ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground')} aria-current={active === section.id ? 'page' : undefined} onclick={() => active = section.id}>{section.title}</button>
|
|
31
|
+
{/each}
|
|
32
|
+
</nav>
|
|
33
|
+
<div class="min-w-0">
|
|
34
|
+
{#if active === 'profile'}
|
|
35
|
+
<SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.settingsPlainDescription')} bodyClass="space-y-4">
|
|
36
|
+
<div class="grid gap-4 sm:grid-cols-2"><Input value="Alex Chen" aria-label={i18n.t('profile.name')} /><Input value="Nebula Labs" aria-label="Company" /></div>
|
|
37
|
+
<div><SettingsFieldRow label={i18n.t('account.visibility')} control={visibilityControl} separated /><SettingsFieldRow label={i18n.t('account.availability')} control={availabilityControl} separated /></div>
|
|
38
|
+
</SettingsGroup>
|
|
39
|
+
{:else if active === 'signin'}
|
|
40
|
+
<SettingsGroup title={i18n.t('account.socialSignIn')} description="Connect the providers your team trusts for sign in." bodyClass="flex flex-wrap gap-2"><Button variant="outline">GitHub</Button><Button variant="outline">Google</Button></SettingsGroup>
|
|
41
|
+
{:else if active === 'preferences'}
|
|
42
|
+
<SettingsGroup title={i18n.t('account.preferences')} bodyClass="grid gap-4 sm:grid-cols-3"><Input value="English" aria-label={i18n.t('account.language')} /><Input value="Asia/Shanghai" aria-label={i18n.t('account.timezone')} /><Input value="CNY" aria-label={i18n.t('account.currency')} /></SettingsGroup>
|
|
43
|
+
{:else}
|
|
44
|
+
<SettingsGroup title={i18n.t('account.manageApi')} description="Use scoped credentials and rotate them when access changes." bodyClass="flex flex-col gap-3 sm:flex-row"><Input readonly value="sv_demo_pub_9f3e********41ab" aria-label="API key" /><Button variant="outline">{i18n.t('common.copy')}</Button></SettingsGroup>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
19
48
|
</ContentPageShell>
|