@svadmin/ui 0.36.0 → 0.37.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 +34 -16
- package/dist/components/AutoForm.svelte +32 -47
- package/dist/components/AutoTable.svelte +26 -31
- package/dist/components/Header.svelte +6 -6
- package/dist/components/Layout.svelte +7 -7
- package/dist/components/PageHeader.svelte +7 -9
- package/dist/components/SettingsPage.svelte +75 -41
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +189 -272
- package/dist/components/SidebarItem.svelte +43 -72
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Component, Snippet } from "svelte";
|
|
3
3
|
import { t } from "@svadmin/core/i18n";
|
|
4
4
|
import { navigate } from "@svadmin/core/router";
|
|
5
|
-
import { User, Info, FileSearch, Lock, Puzzle } from "@lucide/svelte";
|
|
5
|
+
import { User, Palette, Info, Shield, FileSearch, Lock, Puzzle, Bell, Key } from "@lucide/svelte";
|
|
6
6
|
import ProfilePage from "./ProfilePage.svelte";
|
|
7
7
|
import AppearanceSettings from "./AppearanceSettings.svelte";
|
|
8
8
|
import AboutSettings from "./AboutSettings.svelte";
|
|
@@ -60,19 +60,25 @@
|
|
|
60
60
|
{
|
|
61
61
|
group: "settings.general",
|
|
62
62
|
items: [
|
|
63
|
-
{ key: "
|
|
63
|
+
{ key: "profile", path: "/settings/profile", icon: User, label: "settings.profile" },
|
|
64
|
+
{ key: "appearance", path: "/settings/appearance", icon: Palette, label: "settings.appearance" },
|
|
65
|
+
{ key: "notifications", path: "/settings/notifications", icon: Bell, label: "settings.notifications" },
|
|
64
66
|
],
|
|
65
67
|
},
|
|
66
68
|
{
|
|
67
69
|
group: "settings.workspace",
|
|
68
70
|
items: [
|
|
69
|
-
{ key: "
|
|
70
|
-
|
|
71
|
+
{ key: "security", path: "/settings/security", icon: Lock, label: "settings.security" },
|
|
72
|
+
...(authProvider ? [
|
|
73
|
+
{ key: "roles", path: "/settings/roles", icon: Shield, label: "settings.rolesAndPermissions" },
|
|
74
|
+
] : []),
|
|
75
|
+
{ key: "integrations", path: "/settings/integrations", icon: Puzzle, label: "settings.integrations" },
|
|
71
76
|
],
|
|
72
77
|
},
|
|
73
78
|
{
|
|
74
79
|
group: "settings.developer",
|
|
75
80
|
items: [
|
|
81
|
+
{ key: "api", path: "/settings/api", icon: Key, label: "settings.api" },
|
|
76
82
|
...(authProvider ? [
|
|
77
83
|
{ key: "audit", path: "/settings/audit", icon: FileSearch, label: "settings.auditLogs" },
|
|
78
84
|
] : []),
|
|
@@ -86,59 +92,87 @@
|
|
|
86
92
|
|
|
87
93
|
import { getParams, getRoute } from "../router-state.svelte.js";
|
|
88
94
|
|
|
89
|
-
function normalizeKey(tab: string | undefined): string {
|
|
90
|
-
if (!tab) return "account";
|
|
91
|
-
if (["profile", "appearance", "notifications"].includes(tab)) return "account";
|
|
92
|
-
if (["security", "roles"].includes(tab)) return "access";
|
|
93
|
-
if (["integrations", "api"].includes(tab)) return "developer";
|
|
94
|
-
return tab;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function pathForKey(key: string): string {
|
|
98
|
-
return `/settings/${key}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
95
|
let activeKey = $derived.by(() => {
|
|
102
96
|
const tab = getParams().tab;
|
|
103
|
-
|
|
104
|
-
if (sectionKeys.has(
|
|
105
|
-
return content ?
|
|
97
|
+
if (!tab) return "profile";
|
|
98
|
+
if (sectionKeys.has(tab)) return tab;
|
|
99
|
+
return content ? tab : "profile";
|
|
106
100
|
});
|
|
107
101
|
|
|
102
|
+
// Default redirect to profile
|
|
108
103
|
$effect(() => {
|
|
109
104
|
if (getRoute() === "/settings") {
|
|
110
|
-
navigate("/settings/
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const tab = getParams().tab;
|
|
115
|
-
const normalized = normalizeKey(tab);
|
|
116
|
-
if (tab && tab !== normalized && sectionKeys.has(normalized)) {
|
|
117
|
-
navigate(pathForKey(normalized));
|
|
105
|
+
navigate("/settings/profile");
|
|
118
106
|
}
|
|
119
107
|
});
|
|
120
108
|
</script>
|
|
121
109
|
|
|
122
|
-
<div class="min-h-full
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
<div class="flex flex-col lg:flex-row gap-0 min-h-full">
|
|
111
|
+
<!-- Left sidebar navigation -->
|
|
112
|
+
<nav class="w-full lg:w-56 shrink-0 bg-muted/10 shadow-[inset_0_-1px_0_rgba(0,0,0,0.05)] lg:shadow-[inset_-1px_0_0_rgba(0,0,0,0.05)] dark:shadow-[inset_-1px_0_0_rgba(255,255,255,0.05)]">
|
|
113
|
+
<!-- Mobile: horizontal scroll tabs -->
|
|
114
|
+
<div class="flex lg:hidden overflow-x-auto px-4 py-2 gap-1">
|
|
115
|
+
{#each resolvedSections as section, _i (_i)}
|
|
116
|
+
{#each section.items as item, _j (_j)}
|
|
117
|
+
{@const active = activeKey === item.key}
|
|
118
|
+
<button
|
|
119
|
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md whitespace-nowrap transition-colors
|
|
120
|
+
{active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-background/50'}"
|
|
121
|
+
onclick={() => navigate(item.path)}
|
|
122
|
+
>
|
|
123
|
+
<item.icon class="h-4 w-4" />
|
|
124
|
+
{t(item.label)}
|
|
125
|
+
</button>
|
|
126
|
+
{/each}
|
|
127
|
+
{/each}
|
|
127
128
|
</div>
|
|
128
|
-
</div>
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
<!-- Desktop: vertical nav with groups -->
|
|
131
|
+
<div class="hidden lg:block py-6 px-3 space-y-6">
|
|
132
|
+
<div class="px-3">
|
|
133
|
+
<h2 class="text-lg font-semibold text-foreground">{t("settings.title")}</h2>
|
|
134
|
+
<p class="text-xs text-muted-foreground mt-1">{t("settings.settingsDescription")}</p>
|
|
135
|
+
</div>
|
|
136
|
+
{#each resolvedSections as section, _i (_i)}
|
|
137
|
+
<div>
|
|
138
|
+
<h3 class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
139
|
+
{t(section.group)}
|
|
140
|
+
</h3>
|
|
141
|
+
<div class="space-y-0.5">
|
|
142
|
+
{#each section.items as item, _j (_j)}
|
|
143
|
+
{@const active = activeKey === item.key}
|
|
144
|
+
<button
|
|
145
|
+
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all
|
|
146
|
+
{active
|
|
147
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
148
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
149
|
+
onclick={() => navigate(item.path)}
|
|
150
|
+
>
|
|
151
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
152
|
+
{t(item.label)}
|
|
153
|
+
</button>
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
{/each}
|
|
158
|
+
</div>
|
|
159
|
+
</nav>
|
|
160
|
+
|
|
161
|
+
<!-- Right content area -->
|
|
162
|
+
<div class="flex-1 p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
|
|
163
|
+
{#if activeKey === "profile"}
|
|
132
164
|
{#if profile}{@render profile()}{:else}<ProfilePage />{/if}
|
|
165
|
+
{:else if activeKey === "appearance"}
|
|
133
166
|
{#if appearance}{@render appearance()}{:else}<AppearanceSettings />{/if}
|
|
134
|
-
|
|
135
|
-
|
|
167
|
+
{:else if activeKey === "roles"}
|
|
168
|
+
{#if roles}{@render roles()}{:else}<RolesSettings />{/if}
|
|
169
|
+
{:else if activeKey === "security"}
|
|
136
170
|
{#if security}{@render security()}{:else}<SecuritySettings />{/if}
|
|
137
|
-
|
|
138
|
-
{#if roles}{@render roles()}{:else}<RolesSettings />{/if}
|
|
139
|
-
{/if}
|
|
140
|
-
{:else if activeKey === "developer"}
|
|
171
|
+
{:else if activeKey === "integrations"}
|
|
141
172
|
{#if integrations}{@render integrations()}{:else}<IntegrationsSettings />{/if}
|
|
173
|
+
{:else if activeKey === "notifications"}
|
|
174
|
+
{#if notifications}{@render notifications()}{:else}<NotificationsSettings />{/if}
|
|
175
|
+
{:else if activeKey === "api"}
|
|
142
176
|
{#if api}{@render api()}{:else}<ApiSettings />{/if}
|
|
143
177
|
{:else if activeKey === "audit"}
|
|
144
178
|
{#if audit}{@render audit()}{:else}<AuditLogViewer />{/if}
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
</PageHeader>
|
|
39
39
|
|
|
40
40
|
{#if query.isLoading}
|
|
41
|
-
<Card.Root class="overflow-hidden border-border/
|
|
42
|
-
<Card.Content class="
|
|
41
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
42
|
+
<Card.Content class="p-0">
|
|
43
43
|
{#each showFields.slice(0, 6) as _, i (i)}
|
|
44
|
-
<div class="flex flex-col
|
|
44
|
+
<div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
45
45
|
<Skeleton class="h-4 w-1/2 sm:w-1/4" />
|
|
46
46
|
<Skeleton class="h-4 w-3/4 sm:w-2/5 mt-1 sm:mt-0 sm:ml-auto" />
|
|
47
47
|
</div>
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
</Card.Content>
|
|
50
50
|
</Card.Root>
|
|
51
51
|
{:else if query.data?.data}
|
|
52
|
-
<Card.Root class="overflow-hidden border-border/
|
|
53
|
-
<Card.Content class="
|
|
52
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
53
|
+
<Card.Content class="p-0">
|
|
54
54
|
{#each showFields as field, i (i)}
|
|
55
55
|
{@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
|
|
56
56
|
{@const DisplayComponent = getDisplayComponent(field.type)}
|
|
57
|
-
<div class="flex flex-col
|
|
58
|
-
<div class="
|
|
59
|
-
<div class="text-sm text-foreground
|
|
57
|
+
<div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
|
|
58
|
+
<div class="sm:w-1/3 text-xs sm:text-sm font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
|
|
59
|
+
<div class="sm:w-2/3 text-sm text-foreground">
|
|
60
60
|
{#if DisplayComponent && value != null}
|
|
61
61
|
<DisplayComponent
|
|
62
62
|
{value}
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
</Card.Content>
|
|
73
73
|
</Card.Root>
|
|
74
74
|
{:else}
|
|
75
|
-
<Card.Root class="overflow-hidden border-border/
|
|
75
|
+
<Card.Root class="overflow-hidden border-border/40 shadow-sm">
|
|
76
76
|
<Card.Content class="p-8 text-center">
|
|
77
77
|
<p class="text-muted-foreground">{t('common.noData')}</p>
|
|
78
78
|
{#if query.isError}
|