@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.
Files changed (123) hide show
  1. package/dist/app.css +372 -0
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/component-registry.svelte.js +1 -0
  4. package/dist/components/AICommandBar.svelte +3 -3
  5. package/dist/components/AboutSettings.svelte +2 -2
  6. package/dist/components/AdminApp.svelte +96 -18
  7. package/dist/components/AdminApp.svelte.d.ts +32 -2
  8. package/dist/components/AnomalyBadge.svelte +1 -1
  9. package/dist/components/ApiSettings.svelte +191 -0
  10. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  11. package/dist/components/AppearanceSettings.svelte +5 -6
  12. package/dist/components/AuditLogDrawer.svelte +1 -1
  13. package/dist/components/AuditLogViewer.svelte +1 -1
  14. package/dist/components/AutoForm.svelte +50 -44
  15. package/dist/components/AutoTable.svelte +44 -39
  16. package/dist/components/Breadcrumbs.svelte +1 -0
  17. package/dist/components/ChatDialog.svelte +5 -4
  18. package/dist/components/ComboboxField.svelte +3 -3
  19. package/dist/components/CommandPalette.svelte +3 -3
  20. package/dist/components/CopilotPanel.svelte +2 -2
  21. package/dist/components/CreatePage.svelte +0 -1
  22. package/dist/components/DevTools.svelte +4 -3
  23. package/dist/components/DraggableHeader.svelte +2 -1
  24. package/dist/components/EditPage.svelte +1 -1
  25. package/dist/components/EmptyState.svelte +1 -1
  26. package/dist/components/ErrorPage.svelte +58 -0
  27. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  28. package/dist/components/FieldRenderer.svelte +9 -8
  29. package/dist/components/ForgotPasswordPage.svelte +111 -68
  30. package/dist/components/Header.svelte +7 -7
  31. package/dist/components/InferencerPanel.svelte +4 -3
  32. package/dist/components/InfiniteList.svelte +3 -4
  33. package/dist/components/IntegrationsSettings.svelte +70 -0
  34. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  35. package/dist/components/KeyboardShortcuts.svelte +3 -3
  36. package/dist/components/Layout.svelte +13 -13
  37. package/dist/components/LoginPage.svelte +207 -105
  38. package/dist/components/LoginPage.svelte.d.ts +3 -15
  39. package/dist/components/MarkdownRenderer.svelte +3 -5
  40. package/dist/components/ModalForm.svelte +1 -3
  41. package/dist/components/NotificationsSettings.svelte +115 -0
  42. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  43. package/dist/components/PageHeader.svelte +9 -7
  44. package/dist/components/PageSkeleton.svelte +4 -4
  45. package/dist/components/PasswordInput.svelte +1 -2
  46. package/dist/components/PermissionMatrix.svelte +5 -5
  47. package/dist/components/ProfilePage.svelte +1 -1
  48. package/dist/components/RegisterPage.svelte +103 -65
  49. package/dist/components/ResourceOperationsPage.svelte +153 -0
  50. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  51. package/dist/components/RolesSettings.svelte +1 -2
  52. package/dist/components/SecuritySettings.svelte +179 -0
  53. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  54. package/dist/components/SettingsPage.svelte +121 -55
  55. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  56. package/dist/components/ShowPage.svelte +13 -15
  57. package/dist/components/Sidebar.svelte +107 -58
  58. package/dist/components/SidebarItem.svelte +38 -14
  59. package/dist/components/StepsForm.svelte +1 -1
  60. package/dist/components/TaskList.svelte +1 -1
  61. package/dist/components/TaskQueueDrawer.svelte +2 -1
  62. package/dist/components/TenantSwitcher.svelte +1 -2
  63. package/dist/components/Toast.svelte +1 -0
  64. package/dist/components/TooltipButton.svelte +1 -0
  65. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  66. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  67. package/dist/components/UpdatePasswordPage.svelte +86 -48
  68. package/dist/components/VirtualTable.svelte +1 -1
  69. package/dist/components/VoiceInput.svelte +1 -0
  70. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  71. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  72. package/dist/components/account/GetStartedPage.svelte +123 -0
  73. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  74. package/dist/components/account/ImportMembersPage.svelte +143 -0
  75. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  76. package/dist/components/account/MembersStarterPage.svelte +104 -0
  77. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  78. package/dist/components/account/SecurityLogPage.svelte +108 -0
  79. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  80. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  81. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  82. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  83. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  84. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  85. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  86. package/dist/components/account/TeamMembersPage.svelte +98 -0
  87. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  88. package/dist/components/buttons/ImportButton.svelte +1 -1
  89. package/dist/components/buttons/ListButton.svelte +1 -1
  90. package/dist/components/charts/BarChart.svelte +1 -1
  91. package/dist/components/charts/LineChart.svelte +3 -3
  92. package/dist/components/charts/PieChart.svelte +2 -2
  93. package/dist/components/fieldComponentMap.js +2 -1
  94. package/dist/components/fields/ArrayField.svelte +1 -1
  95. package/dist/components/fields/MarkdownField.svelte +1 -0
  96. package/dist/components/fields/MultiSelectField.svelte +1 -1
  97. package/dist/components/fields/TagField.svelte +1 -1
  98. package/dist/components/network/TeamCrewTablePage.svelte +122 -0
  99. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  100. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  101. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  102. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  103. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  104. package/dist/components/profile/ProfileCard.svelte +216 -0
  105. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  106. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  107. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  108. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  109. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  110. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  111. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  112. package/dist/components/ui/alert/index.js +1 -0
  113. package/dist/components/ui/badge/index.js +1 -0
  114. package/dist/components/ui/button/button.svelte +1 -0
  115. package/dist/components/ui/button/index.js +1 -0
  116. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -0
  117. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +1 -1
  118. package/dist/components/ui/sheet/sheet.svelte +1 -0
  119. package/dist/components/ui/tabs/tabs.svelte +1 -0
  120. package/dist/index.d.ts +23 -0
  121. package/dist/index.js +27 -0
  122. package/dist/router-state.svelte.js +17 -0
  123. 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!, resource.code, action.code, !granted)}
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 { UserPlus, User, Loader2, AlertCircle } from '@lucide/svelte';
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 items-center justify-center bg-background p-4">
43
- <div class="w-full max-w-[380px]">
44
- <Card.Card>
45
- <Card.CardHeader class="text-center pb-2">
46
- <div class="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 text-primary mx-auto mb-3">
47
- <UserPlus class="h-5 w-5" />
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
- <Card.CardTitle class="text-xl">{t('auth.createAccount')}</Card.CardTitle>
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
- </Card.CardHeader>
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
- <div class="space-y-2">
62
- <Label for="register-identifier">{t('auth.usernameOrEmail')}</Label>
63
- <div class="relative">
64
- <User class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
65
- <Input
66
- id="register-identifier"
67
- type="text"
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
- <PasswordInput
77
- id="register-password"
78
- label={t('auth.password')}
79
- bind:value={password}
80
- autocomplete="new-password"
81
- showStrength
82
- />
83
-
84
- <PasswordInput
85
- id="register-confirm"
86
- label={t('auth.confirmPassword')}
87
- bind:value={confirmPassword}
88
- autocomplete="new-password"
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
- <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
109
- Powered by {title}
110
- </p>
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>
@@ -0,0 +1,4 @@
1
+ import type { Component } from 'svelte';
2
+ declare const SecuritySettings: Component<Record<string, never>, {}, "">;
3
+ type SecuritySettings = ReturnType<typeof SecuritySettings>;
4
+ export default SecuritySettings;