@svadmin/ui 0.34.9 → 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/component-registry.svelte.js +1 -0
- package/dist/components/AICommandBar.svelte +3 -3
- package/dist/components/AboutSettings.svelte +2 -2
- package/dist/components/AdminApp.svelte +96 -18
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/AnomalyBadge.svelte +1 -1
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AppearanceSettings.svelte +5 -6
- package/dist/components/AuditLogDrawer.svelte +1 -1
- package/dist/components/AuditLogViewer.svelte +1 -1
- package/dist/components/AutoForm.svelte +50 -44
- package/dist/components/AutoTable.svelte +44 -39
- package/dist/components/Breadcrumbs.svelte +1 -0
- package/dist/components/ChatDialog.svelte +5 -4
- package/dist/components/ComboboxField.svelte +3 -3
- package/dist/components/CommandPalette.svelte +3 -3
- package/dist/components/CopilotPanel.svelte +2 -2
- package/dist/components/CreatePage.svelte +0 -1
- package/dist/components/DevTools.svelte +4 -3
- package/dist/components/DraggableHeader.svelte +2 -1
- package/dist/components/EditPage.svelte +1 -1
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +9 -8
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +7 -7
- package/dist/components/InferencerPanel.svelte +4 -3
- package/dist/components/InfiniteList.svelte +3 -4
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/KeyboardShortcuts.svelte +3 -3
- package/dist/components/Layout.svelte +13 -13
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/MarkdownRenderer.svelte +3 -5
- package/dist/components/ModalForm.svelte +1 -3
- 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/PageSkeleton.svelte +4 -4
- package/dist/components/PasswordInput.svelte +1 -2
- package/dist/components/PermissionMatrix.svelte +5 -5
- package/dist/components/ProfilePage.svelte +1 -1
- 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/RolesSettings.svelte +1 -2
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +121 -55
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +13 -15
- package/dist/components/Sidebar.svelte +107 -58
- package/dist/components/SidebarItem.svelte +38 -14
- package/dist/components/StepsForm.svelte +1 -1
- package/dist/components/TaskList.svelte +1 -1
- package/dist/components/TaskQueueDrawer.svelte +2 -1
- package/dist/components/TenantSwitcher.svelte +1 -2
- package/dist/components/Toast.svelte +1 -0
- package/dist/components/TooltipButton.svelte +1 -0
- 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/VirtualTable.svelte +1 -1
- package/dist/components/VoiceInput.svelte +1 -0
- 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/buttons/ImportButton.svelte +1 -1
- package/dist/components/buttons/ListButton.svelte +1 -1
- package/dist/components/charts/BarChart.svelte +1 -1
- package/dist/components/charts/LineChart.svelte +3 -3
- package/dist/components/charts/PieChart.svelte +2 -2
- package/dist/components/fieldComponentMap.js +2 -1
- package/dist/components/fields/ArrayField.svelte +1 -1
- package/dist/components/fields/MarkdownField.svelte +1 -0
- package/dist/components/fields/MultiSelectField.svelte +1 -1
- package/dist/components/fields/TagField.svelte +1 -1
- 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/components/ui/alert/index.js +1 -0
- package/dist/components/ui/badge/index.js +1 -0
- package/dist/components/ui/button/button.svelte +1 -0
- package/dist/components/ui/button/index.js +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +1 -1
- package/dist/components/ui/sheet/sheet.svelte +1 -0
- package/dist/components/ui/tabs/tabs.svelte +1 -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 +13 -13
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
<div class="w-full md:w-64 flex-shrink-0 bg-card rounded-xl shadow-sm border border-border p-4 flex flex-col">
|
|
81
81
|
<h2 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4 px-2">{t('permissions.roles') || 'Roles'}</h2>
|
|
82
82
|
<div class="space-y-1 flex-1 overflow-y-auto">
|
|
83
|
-
{#each roles as role}
|
|
83
|
+
{#each roles as role, _i (_i)}
|
|
84
84
|
<button
|
|
85
85
|
onclick={() => selectedRole = role.code}
|
|
86
86
|
class="w-full text-left px-3 py-2.5 rounded-lg text-sm transition-all duration-200 {selectedRole === role.code ? 'bg-primary/10 text-primary font-medium shadow-sm ring-1 ring-primary/20' : 'text-foreground hover:bg-muted'}"
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
<thead class="bg-muted/50">
|
|
134
134
|
<tr>
|
|
135
135
|
<th scope="col" class="py-3 pr-3 pl-4 text-left text-sm font-semibold text-foreground w-48">{t('permissions.resources') || 'Resources'}</th>
|
|
136
|
-
{#each actions as action}
|
|
136
|
+
{#each actions as action, _i (_i)}
|
|
137
137
|
<th scope="col" class="px-2 py-3 text-center text-sm font-semibold text-foreground">
|
|
138
138
|
<div class="flex flex-col items-center">
|
|
139
139
|
<span>{action.name}</span>
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
<td colspan={actions.length + 1} class="py-10 text-center text-muted-foreground">{t('permissions.noResources') || 'No resources available'}</td>
|
|
150
150
|
</tr>
|
|
151
151
|
{/if}
|
|
152
|
-
{#each resources as resource, i}
|
|
152
|
+
{#each resources as resource, i (i)}
|
|
153
153
|
<!-- Section Grouping Header -->
|
|
154
154
|
{#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
|
|
155
155
|
<tr class="bg-primary/5">
|
|
@@ -164,14 +164,14 @@
|
|
|
164
164
|
</td>
|
|
165
165
|
|
|
166
166
|
<!-- Action Checkboxes -->
|
|
167
|
-
{#each actions as action}
|
|
167
|
+
{#each actions as action, _i (_i)}
|
|
168
168
|
{@const granted = selectedRole ? isGranted(selectedRole, resource.code, action.code) : false}
|
|
169
169
|
<td class="whitespace-nowrap px-3 py-3 text-center">
|
|
170
170
|
<div class="flex items-center justify-center">
|
|
171
171
|
<button
|
|
172
172
|
disabled={loading || !selectedRole}
|
|
173
173
|
aria-label="Toggle {action.name}"
|
|
174
|
-
onclick={() => onToggle(selectedRole
|
|
174
|
+
onclick={() => onToggle(selectedRole as string, resource.code, action.code, !granted)}
|
|
175
175
|
class="w-6 h-6 rounded border flex items-center justify-center transition-all focus:outline-none focus:ring-2 focus:ring-primary/50 {granted ? 'bg-primary border-primary text-primary-foreground' : 'border-input bg-background hover:bg-accent'} {loading || !selectedRole ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
|
|
176
176
|
>
|
|
177
177
|
<svg class="w-3.5 h-3.5 {granted ? 'block' : 'hidden'}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
|
|
54
54
|
// Avatar upload
|
|
55
55
|
let avatarUploading = $state(false);
|
|
56
|
-
let fileInput = $state<HTMLInputElement>(null!);
|
|
56
|
+
let fileInput = $state<HTMLInputElement>(null!); // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
|
57
57
|
|
|
58
58
|
async function handleAvatarChange(e: Event) {
|
|
59
59
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
@@ -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;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from '@svadmin/core/i18n';
|
|
3
3
|
import { getAuthProvider, getResources } from '@svadmin/core';
|
|
4
|
-
import type { Role } from '@svadmin/core';
|
|
5
4
|
import PermissionMatrix from './PermissionMatrix.svelte';
|
|
6
5
|
import type { RoleInfo, ResourceInfo, ActionInfo } from '../types.js';
|
|
7
6
|
import { toast } from '@svadmin/core/toast';
|
|
@@ -117,7 +116,7 @@
|
|
|
117
116
|
} catch (e) {
|
|
118
117
|
toast.error((e as Error).message);
|
|
119
118
|
// Rollback on fail
|
|
120
|
-
delete permissionsCache[roleCode];
|
|
119
|
+
delete permissionsCache[roleCode]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
|
|
121
120
|
loadPermissionsForRole(roleCode);
|
|
122
121
|
} finally {
|
|
123
122
|
saving = false;
|
|
@@ -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>
|