@svadmin/ui 0.34.10 → 0.35.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/dist/app.css +372 -0
- package/dist/component-registry.svelte.d.ts +1 -0
- package/dist/components/AdminApp.svelte +94 -15
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AutoForm.svelte +48 -43
- package/dist/components/AutoTable.svelte +27 -22
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +2 -2
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +6 -7
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/Layout.svelte +8 -8
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/NotificationsSettings.svelte +115 -0
- package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +9 -7
- package/dist/components/RegisterPage.svelte +103 -65
- package/dist/components/ResourceOperationsPage.svelte +153 -0
- package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +121 -54
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +96 -48
- package/dist/components/SidebarItem.svelte +37 -12
- package/dist/components/TwoFactorAuthPage.svelte +247 -0
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
- package/dist/components/UpdatePasswordPage.svelte +86 -48
- package/dist/components/account/CompanyProfilePage.svelte +99 -0
- package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
- package/dist/components/account/GetStartedPage.svelte +123 -0
- package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
- package/dist/components/account/ImportMembersPage.svelte +143 -0
- package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
- package/dist/components/account/MembersStarterPage.svelte +104 -0
- package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
- package/dist/components/account/SecurityLogPage.svelte +108 -0
- package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
- package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsPlainPage.svelte +81 -0
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsSidebarPage.svelte +98 -0
- package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
- package/dist/components/account/TeamMembersPage.svelte +98 -0
- package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
- package/dist/components/network/TeamCrewTablePage.svelte +122 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/router-state.svelte.js +17 -0
- package/package.json +2 -2
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
import { Button } from './ui/button/index.js';
|
|
6
6
|
import { Input } from './ui/input/index.js';
|
|
7
7
|
import { Label } from './ui/label/index.js';
|
|
8
|
-
import * as Card from './ui/card/index.js';
|
|
9
8
|
import * as Alert from './ui/alert/index.js';
|
|
10
9
|
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
-
import {
|
|
10
|
+
import { User, Loader2, AlertCircle, Shield } from '@lucide/svelte';
|
|
12
11
|
|
|
13
12
|
let { title = 'Admin', onSuccess } = $props<{
|
|
14
13
|
title?: string;
|
|
@@ -39,74 +38,113 @@
|
|
|
39
38
|
}
|
|
40
39
|
</script>
|
|
41
40
|
|
|
42
|
-
<div class="min-h-screen flex
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
<div class="min-h-screen flex flex-col lg:flex-row">
|
|
42
|
+
<!-- Left brand panel (mirrors LoginPage) -->
|
|
43
|
+
<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">
|
|
44
|
+
<div class="absolute inset-0">
|
|
45
|
+
<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>
|
|
46
|
+
<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>
|
|
47
|
+
<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>
|
|
48
|
+
<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>
|
|
49
|
+
<div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
|
|
53
|
+
<div class="flex items-center gap-3">
|
|
54
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 backdrop-blur-sm">
|
|
55
|
+
<Shield class="h-5 w-5 text-white" />
|
|
56
|
+
</div>
|
|
57
|
+
<span class="text-lg font-semibold text-white tracking-tight">{title}</span>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="space-y-4">
|
|
61
|
+
<h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
|
|
62
|
+
{t('auth.createAccount')}
|
|
63
|
+
</h2>
|
|
64
|
+
<p class="text-base text-white/70 leading-relaxed max-w-[320px]">
|
|
65
|
+
{t('auth.createAccountMessage')}
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div class="flex items-center gap-3">
|
|
70
|
+
<div class="h-1 w-8 rounded-full bg-white/30"></div>
|
|
71
|
+
<div class="h-1 w-4 rounded-full bg-white/15"></div>
|
|
72
|
+
<div class="h-1 w-2 rounded-full bg-white/10"></div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Right form panel -->
|
|
78
|
+
<div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
|
|
79
|
+
<div class="w-full max-w-[400px]">
|
|
80
|
+
<div class="flex items-center gap-3 mb-8 lg:hidden">
|
|
81
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
82
|
+
<Shield class="h-5 w-5" />
|
|
48
83
|
</div>
|
|
49
|
-
<
|
|
84
|
+
<span class="text-lg font-semibold tracking-tight">{title}</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="space-y-1 mb-8">
|
|
88
|
+
<h1 class="text-2xl font-semibold tracking-tight">{t('auth.createAccount')}</h1>
|
|
50
89
|
<p class="text-sm text-muted-foreground">{t('auth.createAccountMessage')}</p>
|
|
51
|
-
</
|
|
52
|
-
<Card.CardContent>
|
|
53
|
-
<form onsubmit={handleSubmit} class="space-y-4">
|
|
54
|
-
{#if error}
|
|
55
|
-
<Alert.Root variant="destructive">
|
|
56
|
-
<AlertCircle class="h-4 w-4" />
|
|
57
|
-
<Alert.Description>{error}</Alert.Description>
|
|
58
|
-
</Alert.Root>
|
|
59
|
-
{/if}
|
|
90
|
+
</div>
|
|
60
91
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
placeholder={t('auth.identifierPlaceholder')}
|
|
69
|
-
bind:value={identifier}
|
|
70
|
-
class="pl-9"
|
|
71
|
-
autocomplete="username"
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
92
|
+
<form onsubmit={handleSubmit} class="space-y-5">
|
|
93
|
+
{#if error}
|
|
94
|
+
<Alert.Root variant="destructive">
|
|
95
|
+
<AlertCircle class="h-4 w-4" />
|
|
96
|
+
<Alert.Description>{error}</Alert.Description>
|
|
97
|
+
</Alert.Root>
|
|
98
|
+
{/if}
|
|
75
99
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/>
|
|
90
|
-
|
|
91
|
-
<Button type="submit" class="w-full" disabled={register.isLoading}>
|
|
92
|
-
{#if register.isLoading}
|
|
93
|
-
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
94
|
-
{/if}
|
|
95
|
-
{t('auth.registerButton')}
|
|
96
|
-
</Button>
|
|
97
|
-
</form>
|
|
98
|
-
|
|
99
|
-
<div class="flex items-center justify-center gap-1 mt-5 pt-5 border-t">
|
|
100
|
-
<span class="text-sm text-muted-foreground">{t('auth.hasAccount')}</span>
|
|
101
|
-
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
|
|
102
|
-
{t('auth.login')}
|
|
103
|
-
</Button>
|
|
100
|
+
<div class="space-y-2">
|
|
101
|
+
<Label for="register-identifier">{t('auth.usernameOrEmail')}</Label>
|
|
102
|
+
<div class="relative">
|
|
103
|
+
<User class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
|
|
104
|
+
<Input
|
|
105
|
+
id="register-identifier"
|
|
106
|
+
type="text"
|
|
107
|
+
placeholder={t('auth.identifierPlaceholder')}
|
|
108
|
+
bind:value={identifier}
|
|
109
|
+
class="pl-9"
|
|
110
|
+
autocomplete="username"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
104
113
|
</div>
|
|
105
|
-
</Card.CardContent>
|
|
106
|
-
</Card.Card>
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
<PasswordInput
|
|
116
|
+
id="register-password"
|
|
117
|
+
label={t('auth.password')}
|
|
118
|
+
bind:value={password}
|
|
119
|
+
autocomplete="new-password"
|
|
120
|
+
showStrength
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<PasswordInput
|
|
124
|
+
id="register-confirm"
|
|
125
|
+
label={t('auth.confirmPassword')}
|
|
126
|
+
bind:value={confirmPassword}
|
|
127
|
+
autocomplete="new-password"
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
<Button type="submit" class="w-full h-10" disabled={register.isLoading}>
|
|
131
|
+
{#if register.isLoading}
|
|
132
|
+
<Loader2 class="h-4 w-4 animate-spin mr-2" />
|
|
133
|
+
{/if}
|
|
134
|
+
{t('auth.registerButton')}
|
|
135
|
+
</Button>
|
|
136
|
+
</form>
|
|
137
|
+
|
|
138
|
+
<div class="flex items-center justify-center gap-1 mt-8 pt-6 border-t">
|
|
139
|
+
<span class="text-sm text-muted-foreground">{t('auth.hasAccount')}</span>
|
|
140
|
+
<Button variant="link" class="text-sm h-auto p-0 font-medium" onclick={() => navigate('/login')}>
|
|
141
|
+
{t('auth.login')}
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<p class="text-xs text-muted-foreground/60 mt-8 text-center">
|
|
146
|
+
Powered by {title}
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
111
149
|
</div>
|
|
112
150
|
</div>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { navigate } from '@svadmin/core/router';
|
|
3
|
+
import type { Component } from 'svelte';
|
|
4
|
+
import AutoTable from './AutoTable.svelte';
|
|
5
|
+
import { Badge } from './ui/badge/index.js';
|
|
6
|
+
import { Button } from './ui/button/index.js';
|
|
7
|
+
import * as Card from './ui/card/index.js';
|
|
8
|
+
|
|
9
|
+
interface SummaryMetric {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string | number;
|
|
12
|
+
hint?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface StatusLane {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string | number;
|
|
18
|
+
hint?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface HighlightItem {
|
|
22
|
+
title: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
meta?: string;
|
|
25
|
+
badge?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
resourceName: string;
|
|
30
|
+
eyebrow: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
actionLabel: string;
|
|
34
|
+
icon?: Component<{ class?: string }>;
|
|
35
|
+
metrics?: SummaryMetric[];
|
|
36
|
+
lanes?: StatusLane[];
|
|
37
|
+
highlights?: HighlightItem[];
|
|
38
|
+
tableLabel?: string;
|
|
39
|
+
tableDescription?: string;
|
|
40
|
+
emptyLanesText?: string;
|
|
41
|
+
highlightsLabel?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
resourceName,
|
|
46
|
+
eyebrow,
|
|
47
|
+
title,
|
|
48
|
+
description,
|
|
49
|
+
actionLabel,
|
|
50
|
+
icon: Icon,
|
|
51
|
+
metrics = [],
|
|
52
|
+
lanes = [],
|
|
53
|
+
highlights = [],
|
|
54
|
+
tableLabel = 'Records',
|
|
55
|
+
tableDescription,
|
|
56
|
+
emptyLanesText = 'No grouped records yet.',
|
|
57
|
+
highlightsLabel = 'Focus queue',
|
|
58
|
+
}: Props = $props();
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<div class="space-y-6" data-svadmin-resource-operations={resourceName}>
|
|
62
|
+
<section class="grid gap-4 xl:grid-cols-[0.82fr_1.18fr]">
|
|
63
|
+
<Card.Root class="rounded-[2rem] border-border/70 bg-card">
|
|
64
|
+
<Card.Content class="p-6 sm:p-8">
|
|
65
|
+
<Badge class="mb-4 rounded-full bg-primary/10 text-primary hover:bg-primary/10">{eyebrow}</Badge>
|
|
66
|
+
<div class="flex items-start gap-4">
|
|
67
|
+
{#if Icon}
|
|
68
|
+
<span class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm shadow-primary/25">
|
|
69
|
+
<Icon class="h-6 w-6" />
|
|
70
|
+
</span>
|
|
71
|
+
{/if}
|
|
72
|
+
<div class="min-w-0">
|
|
73
|
+
<h1 class="text-3xl font-semibold tracking-tight">{title}</h1>
|
|
74
|
+
<p class="mt-2 max-w-2xl text-sm text-muted-foreground">{description}</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<Button class="mt-6 rounded-xl" onclick={() => navigate(`/${resourceName}/create`)}>
|
|
78
|
+
{actionLabel}
|
|
79
|
+
</Button>
|
|
80
|
+
</Card.Content>
|
|
81
|
+
</Card.Root>
|
|
82
|
+
|
|
83
|
+
<div class="grid gap-4 md:grid-cols-3">
|
|
84
|
+
{#each metrics as metric (metric.label)}
|
|
85
|
+
<Card.Root class="rounded-[2rem] border-border/70 bg-card">
|
|
86
|
+
<Card.Content class="p-5">
|
|
87
|
+
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">{metric.label}</p>
|
|
88
|
+
<p class="mt-2 text-3xl font-semibold">{metric.value}</p>
|
|
89
|
+
{#if metric.hint}
|
|
90
|
+
<p class="mt-1 text-xs text-muted-foreground">{metric.hint}</p>
|
|
91
|
+
{/if}
|
|
92
|
+
</Card.Content>
|
|
93
|
+
</Card.Root>
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<div class="grid gap-4 xl:grid-cols-[0.76fr_1.24fr]">
|
|
99
|
+
<div class="space-y-4">
|
|
100
|
+
<Card.Root class="rounded-[2rem] border-border/70 bg-card">
|
|
101
|
+
<Card.Header>
|
|
102
|
+
<Card.Title>{tableLabel}</Card.Title>
|
|
103
|
+
{#if tableDescription}
|
|
104
|
+
<Card.Description>{tableDescription}</Card.Description>
|
|
105
|
+
{/if}
|
|
106
|
+
</Card.Header>
|
|
107
|
+
<Card.Content class="space-y-3">
|
|
108
|
+
{#each lanes as lane (lane.label)}
|
|
109
|
+
<div class="rounded-2xl border border-border/60 bg-muted/20 p-4">
|
|
110
|
+
<div class="flex items-center justify-between gap-3">
|
|
111
|
+
<p class="font-medium capitalize">{lane.label}</p>
|
|
112
|
+
<Badge variant="outline">{lane.value}</Badge>
|
|
113
|
+
</div>
|
|
114
|
+
{#if lane.hint}
|
|
115
|
+
<p class="mt-1 text-xs text-muted-foreground">{lane.hint}</p>
|
|
116
|
+
{/if}
|
|
117
|
+
</div>
|
|
118
|
+
{:else}
|
|
119
|
+
<p class="rounded-2xl border border-border/60 bg-muted/20 p-4 text-sm text-muted-foreground">{emptyLanesText}</p>
|
|
120
|
+
{/each}
|
|
121
|
+
</Card.Content>
|
|
122
|
+
</Card.Root>
|
|
123
|
+
|
|
124
|
+
{#if highlights.length > 0}
|
|
125
|
+
<Card.Root class="rounded-[2rem] border-primary/20 bg-primary/5">
|
|
126
|
+
<Card.Header>
|
|
127
|
+
<Card.Title>{highlightsLabel}</Card.Title>
|
|
128
|
+
</Card.Header>
|
|
129
|
+
<Card.Content class="space-y-3">
|
|
130
|
+
{#each highlights as item (item.title)}
|
|
131
|
+
<div class="rounded-2xl border border-primary/15 bg-background/80 p-4">
|
|
132
|
+
<div class="flex items-center justify-between gap-3">
|
|
133
|
+
<p class="font-medium">{item.title}</p>
|
|
134
|
+
{#if item.badge}
|
|
135
|
+
<Badge>{item.badge}</Badge>
|
|
136
|
+
{/if}
|
|
137
|
+
</div>
|
|
138
|
+
{#if item.description}
|
|
139
|
+
<p class="mt-1 text-xs text-muted-foreground">{item.description}</p>
|
|
140
|
+
{/if}
|
|
141
|
+
{#if item.meta}
|
|
142
|
+
<p class="mt-2 text-xs font-medium text-primary">{item.meta}</p>
|
|
143
|
+
{/if}
|
|
144
|
+
</div>
|
|
145
|
+
{/each}
|
|
146
|
+
</Card.Content>
|
|
147
|
+
</Card.Root>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<AutoTable {resourceName} />
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
interface SummaryMetric {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string | number;
|
|
5
|
+
hint?: string;
|
|
6
|
+
}
|
|
7
|
+
interface StatusLane {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string | number;
|
|
10
|
+
hint?: string;
|
|
11
|
+
}
|
|
12
|
+
interface HighlightItem {
|
|
13
|
+
title: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
meta?: string;
|
|
16
|
+
badge?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Props {
|
|
19
|
+
resourceName: string;
|
|
20
|
+
eyebrow: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
actionLabel: string;
|
|
24
|
+
icon?: Component<{
|
|
25
|
+
class?: string;
|
|
26
|
+
}>;
|
|
27
|
+
metrics?: SummaryMetric[];
|
|
28
|
+
lanes?: StatusLane[];
|
|
29
|
+
highlights?: HighlightItem[];
|
|
30
|
+
tableLabel?: string;
|
|
31
|
+
tableDescription?: string;
|
|
32
|
+
emptyLanesText?: string;
|
|
33
|
+
highlightsLabel?: string;
|
|
34
|
+
}
|
|
35
|
+
declare const ResourceOperationsPage: Component<Props, {}, "">;
|
|
36
|
+
type ResourceOperationsPage = ReturnType<typeof ResourceOperationsPage>;
|
|
37
|
+
export default ResourceOperationsPage;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import { AlertTriangle, CheckCircle2, KeyRound, Monitor, Shield, ShieldAlert, ShieldCheck, Smartphone, Trash2 } from '@lucide/svelte';
|
|
4
|
+
import type { Component } from 'svelte';
|
|
5
|
+
import * as Alert from './ui/alert/index.js';
|
|
6
|
+
import * as Card from './ui/card/index.js';
|
|
7
|
+
import { Button } from './ui/button/index.js';
|
|
8
|
+
import { Label } from './ui/label/index.js';
|
|
9
|
+
import { Switch } from './ui/switch/index.js';
|
|
10
|
+
import PasswordInput from './PasswordInput.svelte';
|
|
11
|
+
|
|
12
|
+
interface SessionInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
os: string;
|
|
15
|
+
browser: string;
|
|
16
|
+
ip: string;
|
|
17
|
+
lastActive: string;
|
|
18
|
+
isCurrent: boolean;
|
|
19
|
+
Icon: Component;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let is2faEnabled = $state(false);
|
|
23
|
+
let sessions = $state<SessionInfo[]>([
|
|
24
|
+
{ id: 'current', os: 'macOS', browser: 'Chrome', ip: '192.168.1.102', lastActive: 'Active now', isCurrent: true, Icon: Monitor },
|
|
25
|
+
{ id: 'mobile', os: 'iOS', browser: 'Safari', ip: '192.168.1.105', lastActive: '2 hours ago', isCurrent: false, Icon: Smartphone },
|
|
26
|
+
{ id: 'desktop', os: 'Windows', browser: 'Edge', ip: '10.0.0.4', lastActive: '2 days ago', isCurrent: false, Icon: Monitor },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
let currentPassword = $state('');
|
|
30
|
+
let newPassword = $state('');
|
|
31
|
+
let confirmPassword = $state('');
|
|
32
|
+
let passwordSuccessMessage = $state('');
|
|
33
|
+
let passwordErrorMessage = $state('');
|
|
34
|
+
let lastChanged = $state('3 months ago');
|
|
35
|
+
|
|
36
|
+
function handlePasswordChange(event: SubmitEvent) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
passwordSuccessMessage = '';
|
|
39
|
+
passwordErrorMessage = '';
|
|
40
|
+
|
|
41
|
+
if (!currentPassword) {
|
|
42
|
+
passwordErrorMessage = t('profile.currentPassword');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (newPassword.length < 8) {
|
|
46
|
+
passwordErrorMessage = t('validation.minLength', { min: 8 });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (newPassword !== confirmPassword) {
|
|
50
|
+
passwordErrorMessage = t('auth.passwordMismatch');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
passwordSuccessMessage = t('profile.passwordChanged');
|
|
55
|
+
lastChanged = 'Just now';
|
|
56
|
+
currentPassword = '';
|
|
57
|
+
newPassword = '';
|
|
58
|
+
confirmPassword = '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function revokeSession(id: string) {
|
|
62
|
+
sessions = sessions.filter((session) => session.id !== id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function revokeAllOthers() {
|
|
66
|
+
sessions = sessions.filter((session) => session.isCurrent);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div class="space-y-6">
|
|
71
|
+
<div>
|
|
72
|
+
<h2 class="text-xl font-semibold text-foreground">{t('settings.security')}</h2>
|
|
73
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('settings.securityDescription')}</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<Card.Card>
|
|
77
|
+
<Card.CardHeader>
|
|
78
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
79
|
+
{#if is2faEnabled}
|
|
80
|
+
<ShieldCheck class="h-5 w-5 text-green-500" />
|
|
81
|
+
{:else}
|
|
82
|
+
<ShieldAlert class="h-5 w-5 text-amber-500" />
|
|
83
|
+
{/if}
|
|
84
|
+
{t('security.twoFactorAuth')}
|
|
85
|
+
</Card.CardTitle>
|
|
86
|
+
<Card.CardDescription>{t('security.twoFactorDescription')}</Card.CardDescription>
|
|
87
|
+
</Card.CardHeader>
|
|
88
|
+
<Card.CardContent>
|
|
89
|
+
<div class="flex items-center justify-between gap-4 border-t pt-4">
|
|
90
|
+
<div class="space-y-1">
|
|
91
|
+
<Label for="two-factor" class="text-sm font-medium">{t('security.enable2fa')}</Label>
|
|
92
|
+
<p class="text-xs text-muted-foreground">
|
|
93
|
+
{is2faEnabled ? t('security.twoFactorActive') : t('security.twoFactorInactive')}
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
<Switch id="two-factor" bind:checked={is2faEnabled} />
|
|
97
|
+
</div>
|
|
98
|
+
</Card.CardContent>
|
|
99
|
+
</Card.Card>
|
|
100
|
+
|
|
101
|
+
<Card.Card>
|
|
102
|
+
<Card.CardHeader>
|
|
103
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
104
|
+
<KeyRound class="h-4 w-4 text-muted-foreground" />
|
|
105
|
+
{t('profile.changePassword')}
|
|
106
|
+
</Card.CardTitle>
|
|
107
|
+
<Card.CardDescription>{t('security.passwordLastChanged', { time: lastChanged })}</Card.CardDescription>
|
|
108
|
+
</Card.CardHeader>
|
|
109
|
+
<Card.CardContent>
|
|
110
|
+
<form onsubmit={handlePasswordChange} class="max-w-sm space-y-4">
|
|
111
|
+
{#if passwordSuccessMessage}
|
|
112
|
+
<Alert.Root class="border-success/30 bg-success/5 text-success">
|
|
113
|
+
<CheckCircle2 class="h-4 w-4" />
|
|
114
|
+
<Alert.Description>{passwordSuccessMessage}</Alert.Description>
|
|
115
|
+
</Alert.Root>
|
|
116
|
+
{/if}
|
|
117
|
+
{#if passwordErrorMessage}
|
|
118
|
+
<Alert.Root variant="destructive">
|
|
119
|
+
<AlertTriangle class="h-4 w-4" />
|
|
120
|
+
<Alert.Description>{passwordErrorMessage}</Alert.Description>
|
|
121
|
+
</Alert.Root>
|
|
122
|
+
{/if}
|
|
123
|
+
|
|
124
|
+
<PasswordInput id="security-current-password" label={t('profile.currentPassword')} bind:value={currentPassword} autocomplete="current-password" />
|
|
125
|
+
<PasswordInput id="security-new-password" label={t('profile.newPassword')} bind:value={newPassword} autocomplete="new-password" showStrength />
|
|
126
|
+
<PasswordInput id="security-confirm-password" label={t('auth.confirmPassword')} bind:value={confirmPassword} autocomplete="new-password" />
|
|
127
|
+
|
|
128
|
+
<Button type="submit">{t('profile.updatePassword')}</Button>
|
|
129
|
+
</form>
|
|
130
|
+
</Card.CardContent>
|
|
131
|
+
</Card.Card>
|
|
132
|
+
|
|
133
|
+
<Card.Card>
|
|
134
|
+
<Card.CardHeader class="flex flex-row items-start justify-between gap-4">
|
|
135
|
+
<div class="space-y-1">
|
|
136
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
137
|
+
<Shield class="h-4 w-4 text-muted-foreground" />
|
|
138
|
+
{t('security.activeSessions')}
|
|
139
|
+
</Card.CardTitle>
|
|
140
|
+
<Card.CardDescription>{t('security.sessionsDescription')}</Card.CardDescription>
|
|
141
|
+
</div>
|
|
142
|
+
{#if sessions.length > 1}
|
|
143
|
+
<Button variant="outline" size="sm" onclick={revokeAllOthers}>{t('security.revokeOthers')}</Button>
|
|
144
|
+
{/if}
|
|
145
|
+
</Card.CardHeader>
|
|
146
|
+
<Card.CardContent>
|
|
147
|
+
<div class="divide-y">
|
|
148
|
+
{#each sessions as session (session.id)}
|
|
149
|
+
<div class="flex items-center justify-between gap-4 py-3">
|
|
150
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
151
|
+
<div class="rounded-lg bg-muted p-2 text-muted-foreground">
|
|
152
|
+
<session.Icon class="h-5 w-5" />
|
|
153
|
+
</div>
|
|
154
|
+
<div class="min-w-0 space-y-1">
|
|
155
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
156
|
+
<span class="text-sm font-medium">{session.os} - {session.browser}</span>
|
|
157
|
+
{#if session.isCurrent}
|
|
158
|
+
<span class="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary">{t('security.currentSession')}</span>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
162
|
+
<span>{session.ip}</span>
|
|
163
|
+
<span>{session.lastActive}</span>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{#if !session.isCurrent}
|
|
168
|
+
<Button variant="ghost" size="icon-sm" onclick={() => revokeSession(session.id)} title={t('security.revokeSession')}>
|
|
169
|
+
<Trash2 class="h-4 w-4" />
|
|
170
|
+
</Button>
|
|
171
|
+
{/if}
|
|
172
|
+
</div>
|
|
173
|
+
{:else}
|
|
174
|
+
<div class="py-6 text-center text-sm text-muted-foreground">{t('security.noSessions')}</div>
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
</Card.CardContent>
|
|
178
|
+
</Card.Card>
|
|
179
|
+
</div>
|