@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
|
@@ -1,66 +1,123 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
2
|
+
import type { Component, Snippet } from "svelte";
|
|
3
|
+
import { t } from "@svadmin/core/i18n";
|
|
4
|
+
import { navigate } from "@svadmin/core/router";
|
|
5
|
+
import { User, Palette, Info, Shield, FileSearch, Lock, Puzzle, Bell, Key } from "@lucide/svelte";
|
|
6
|
+
import ProfilePage from "./ProfilePage.svelte";
|
|
7
|
+
import AppearanceSettings from "./AppearanceSettings.svelte";
|
|
8
|
+
import AboutSettings from "./AboutSettings.svelte";
|
|
9
|
+
import RolesSettings from "./RolesSettings.svelte";
|
|
10
|
+
import AuditLogViewer from "./AuditLogViewer.svelte";
|
|
11
|
+
import SecuritySettings from "./SecuritySettings.svelte";
|
|
12
|
+
import IntegrationsSettings from "./IntegrationsSettings.svelte";
|
|
13
|
+
import NotificationsSettings from "./NotificationsSettings.svelte";
|
|
14
|
+
import ApiSettings from "./ApiSettings.svelte";
|
|
15
|
+
import { getAuthProvider } from "@svadmin/core";
|
|
11
16
|
|
|
12
17
|
const authProvider = getAuthProvider({ optional: true });
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
interface SettingsItem {
|
|
20
|
+
key: string;
|
|
21
|
+
path: string;
|
|
22
|
+
icon: Component;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SettingsSection {
|
|
27
|
+
group: string;
|
|
28
|
+
items: SettingsItem[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
customSections?: SettingsSection[];
|
|
33
|
+
profile?: Snippet;
|
|
34
|
+
appearance?: Snippet;
|
|
35
|
+
roles?: Snippet;
|
|
36
|
+
security?: Snippet;
|
|
37
|
+
integrations?: Snippet;
|
|
38
|
+
notifications?: Snippet;
|
|
39
|
+
api?: Snippet;
|
|
40
|
+
audit?: Snippet;
|
|
41
|
+
about?: Snippet;
|
|
42
|
+
content?: Snippet<[string]>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let {
|
|
46
|
+
customSections,
|
|
47
|
+
profile,
|
|
48
|
+
appearance,
|
|
49
|
+
roles,
|
|
50
|
+
security,
|
|
51
|
+
integrations,
|
|
52
|
+
notifications,
|
|
53
|
+
api,
|
|
54
|
+
audit,
|
|
55
|
+
about,
|
|
56
|
+
content,
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
|
|
59
|
+
const defaultSections = [
|
|
15
60
|
{
|
|
16
|
-
group:
|
|
61
|
+
group: "settings.general",
|
|
17
62
|
items: [
|
|
18
|
-
{ key:
|
|
19
|
-
{ 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" },
|
|
20
66
|
],
|
|
21
67
|
},
|
|
22
68
|
{
|
|
23
|
-
group:
|
|
69
|
+
group: "settings.workspace",
|
|
24
70
|
items: [
|
|
71
|
+
{ key: "security", path: "/settings/security", icon: Lock, label: "settings.security" },
|
|
25
72
|
...(authProvider ? [
|
|
26
|
-
{ key:
|
|
27
|
-
{ key: 'audit', path: '/settings/audit', icon: FileSearch, label: 'settings.auditLogs' },
|
|
73
|
+
{ key: "roles", path: "/settings/roles", icon: Shield, label: "settings.rolesAndPermissions" },
|
|
28
74
|
] : []),
|
|
29
|
-
{ key:
|
|
75
|
+
{ key: "integrations", path: "/settings/integrations", icon: Puzzle, label: "settings.integrations" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
group: "settings.developer",
|
|
80
|
+
items: [
|
|
81
|
+
{ key: "api", path: "/settings/api", icon: Key, label: "settings.api" },
|
|
82
|
+
...(authProvider ? [
|
|
83
|
+
{ key: "audit", path: "/settings/audit", icon: FileSearch, label: "settings.auditLogs" },
|
|
84
|
+
] : []),
|
|
85
|
+
{ key: "about", path: "/settings/about", icon: Info, label: "settings.about" },
|
|
30
86
|
],
|
|
31
87
|
},
|
|
32
88
|
];
|
|
33
89
|
|
|
34
|
-
|
|
90
|
+
const resolvedSections = $derived(customSections ?? defaultSections);
|
|
91
|
+
const sectionKeys = $derived(new Set(resolvedSections.flatMap((section) => section.items.map((item) => item.key))));
|
|
92
|
+
|
|
93
|
+
import { getParams, getRoute } from "../router-state.svelte.js";
|
|
35
94
|
|
|
36
95
|
let activeKey = $derived.by(() => {
|
|
37
96
|
const tab = getParams().tab;
|
|
38
|
-
if (tab
|
|
39
|
-
if (tab
|
|
40
|
-
|
|
41
|
-
if (tab === 'about') return 'about';
|
|
42
|
-
return 'profile';
|
|
97
|
+
if (!tab) return "profile";
|
|
98
|
+
if (sectionKeys.has(tab)) return tab;
|
|
99
|
+
return content ? tab : "profile";
|
|
43
100
|
});
|
|
44
101
|
|
|
45
102
|
// Default redirect to profile
|
|
46
103
|
$effect(() => {
|
|
47
|
-
if (getRoute() ===
|
|
48
|
-
navigate(
|
|
104
|
+
if (getRoute() === "/settings") {
|
|
105
|
+
navigate("/settings/profile");
|
|
49
106
|
}
|
|
50
107
|
});
|
|
51
108
|
</script>
|
|
52
109
|
|
|
53
|
-
<div class="flex flex-col
|
|
110
|
+
<div class="flex min-h-full flex-col overflow-hidden rounded-3xl border border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03] backdrop-blur lg:flex-row">
|
|
54
111
|
<!-- Left sidebar navigation -->
|
|
55
|
-
<nav class="w-full
|
|
112
|
+
<nav class="w-full shrink-0 border-b border-border/70 bg-muted/20 lg:w-64 lg:border-b-0 lg:border-r">
|
|
56
113
|
<!-- Mobile: horizontal scroll tabs -->
|
|
57
|
-
<div class="flex
|
|
58
|
-
{#each
|
|
59
|
-
{#each section.items as item,
|
|
114
|
+
<div class="flex gap-2 overflow-x-auto px-4 py-3 lg:hidden">
|
|
115
|
+
{#each resolvedSections as section, _i (_i)}
|
|
116
|
+
{#each section.items as item, _j (_j)}
|
|
60
117
|
{@const active = activeKey === item.key}
|
|
61
118
|
<button
|
|
62
|
-
class="flex items-center gap-2 px-3 py-2 text-sm font-medium
|
|
63
|
-
{active ? 'bg-
|
|
119
|
+
class="flex items-center gap-2 whitespace-nowrap rounded-xl px-3 py-2 text-sm font-medium transition-colors
|
|
120
|
+
{active ? 'bg-primary/10 text-primary ring-1 ring-primary/15' : 'text-muted-foreground hover:bg-background/70 hover:text-foreground'}"
|
|
64
121
|
onclick={() => navigate(item.path)}
|
|
65
122
|
>
|
|
66
123
|
<item.icon class="h-4 w-4" />
|
|
@@ -71,24 +128,24 @@
|
|
|
71
128
|
</div>
|
|
72
129
|
|
|
73
130
|
<!-- Desktop: vertical nav with groups -->
|
|
74
|
-
<div class="hidden
|
|
75
|
-
<div class="
|
|
76
|
-
<h2 class="text-lg font-semibold text-foreground">{t(
|
|
77
|
-
<p class="text-xs text-muted-foreground mt-1">{t(
|
|
131
|
+
<div class="hidden space-y-6 px-4 py-6 lg:block">
|
|
132
|
+
<div class="rounded-2xl border border-border/60 bg-background/70 p-4">
|
|
133
|
+
<h2 class="text-lg font-semibold tracking-tight text-foreground">{t("settings.title")}</h2>
|
|
134
|
+
<p class="text-xs text-muted-foreground mt-1">{t("settings.settingsDescription")}</p>
|
|
78
135
|
</div>
|
|
79
|
-
{#each
|
|
136
|
+
{#each resolvedSections as section, _i (_i)}
|
|
80
137
|
<div>
|
|
81
|
-
<h3 class="px-3
|
|
138
|
+
<h3 class="mb-2 px-3 text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/60">
|
|
82
139
|
{t(section.group)}
|
|
83
140
|
</h3>
|
|
84
|
-
<div class="space-y-
|
|
85
|
-
{#each section.items as item,
|
|
141
|
+
<div class="space-y-1">
|
|
142
|
+
{#each section.items as item, _j (_j)}
|
|
86
143
|
{@const active = activeKey === item.key}
|
|
87
144
|
<button
|
|
88
|
-
class="flex w-full items-center gap-3 rounded-
|
|
145
|
+
class="flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-all
|
|
89
146
|
{active
|
|
90
|
-
? 'bg-primary/10 text-primary font-medium'
|
|
91
|
-
: 'text-muted-foreground hover:bg-
|
|
147
|
+
? 'bg-primary/10 text-primary font-medium ring-1 ring-primary/15 shadow-sm shadow-primary/10'
|
|
148
|
+
: 'text-muted-foreground hover:bg-background/75 hover:text-foreground'}"
|
|
92
149
|
onclick={() => navigate(item.path)}
|
|
93
150
|
>
|
|
94
151
|
<item.icon class="h-4 w-4 shrink-0" />
|
|
@@ -102,17 +159,27 @@
|
|
|
102
159
|
</nav>
|
|
103
160
|
|
|
104
161
|
<!-- Right content area -->
|
|
105
|
-
<div class="flex-1 p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' ? 'max-w-6xl' : 'max-w-3xl'}">
|
|
106
|
-
{#if activeKey ===
|
|
107
|
-
<ProfilePage />
|
|
108
|
-
{:else if activeKey ===
|
|
109
|
-
<AppearanceSettings />
|
|
110
|
-
{:else if activeKey ===
|
|
111
|
-
<RolesSettings />
|
|
112
|
-
{:else if activeKey ===
|
|
113
|
-
<
|
|
114
|
-
{:else if activeKey ===
|
|
115
|
-
<
|
|
162
|
+
<div class="flex-1 bg-background/60 p-5 sm:p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
|
|
163
|
+
{#if activeKey === "profile"}
|
|
164
|
+
{#if profile}{@render profile()}{:else}<ProfilePage />{/if}
|
|
165
|
+
{:else if activeKey === "appearance"}
|
|
166
|
+
{#if appearance}{@render appearance()}{:else}<AppearanceSettings />{/if}
|
|
167
|
+
{:else if activeKey === "roles"}
|
|
168
|
+
{#if roles}{@render roles()}{:else}<RolesSettings />{/if}
|
|
169
|
+
{:else if activeKey === "security"}
|
|
170
|
+
{#if security}{@render security()}{:else}<SecuritySettings />{/if}
|
|
171
|
+
{:else if activeKey === "integrations"}
|
|
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"}
|
|
176
|
+
{#if api}{@render api()}{:else}<ApiSettings />{/if}
|
|
177
|
+
{:else if activeKey === "audit"}
|
|
178
|
+
{#if audit}{@render audit()}{:else}<AuditLogViewer />{/if}
|
|
179
|
+
{:else if activeKey === "about"}
|
|
180
|
+
{#if about}{@render about()}{:else}<AboutSettings />{/if}
|
|
181
|
+
{:else if content}
|
|
182
|
+
{@render content(activeKey)}
|
|
116
183
|
{/if}
|
|
117
184
|
</div>
|
|
118
185
|
</div>
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
interface SettingsItem {
|
|
3
|
+
key: string;
|
|
4
|
+
path: string;
|
|
5
|
+
icon: Component;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
interface SettingsSection {
|
|
9
|
+
group: string;
|
|
10
|
+
items: SettingsItem[];
|
|
11
|
+
}
|
|
12
|
+
interface Props {
|
|
13
|
+
customSections?: SettingsSection[];
|
|
14
|
+
profile?: Snippet;
|
|
15
|
+
appearance?: Snippet;
|
|
16
|
+
roles?: Snippet;
|
|
17
|
+
security?: Snippet;
|
|
18
|
+
integrations?: Snippet;
|
|
19
|
+
notifications?: Snippet;
|
|
20
|
+
api?: Snippet;
|
|
21
|
+
audit?: Snippet;
|
|
22
|
+
about?: Snippet;
|
|
23
|
+
content?: Snippet<[string]>;
|
|
24
|
+
}
|
|
25
|
+
declare const SettingsPage: Component<Props, {}, "">;
|
|
2
26
|
type SettingsPage = ReturnType<typeof SettingsPage>;
|
|
3
27
|
export default SettingsPage;
|
|
@@ -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="p-
|
|
41
|
+
<Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
|
|
42
|
+
<Card.Content class="space-y-3 p-4 sm:p-5">
|
|
43
43
|
{#each showFields.slice(0, 6) as _, i (i)}
|
|
44
|
-
<div class="flex flex-col
|
|
44
|
+
<div class="flex flex-col rounded-2xl border border-border/50 bg-muted/20 px-4 py-3 sm:flex-row sm:py-4">
|
|
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="p-
|
|
52
|
+
<Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
|
|
53
|
+
<Card.Content class="space-y-3 p-4 sm:p-5">
|
|
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="sm:w-2/3
|
|
57
|
+
<div class="flex flex-col rounded-2xl border border-border/55 bg-background/70 px-4 py-3 transition-colors hover:bg-muted/20 sm:flex-row sm:items-start sm:py-4 {i % 2 === 1 ? 'bg-muted/15' : ''}">
|
|
58
|
+
<div class="mb-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground sm:mb-0 sm:w-1/3">{field.label}</div>
|
|
59
|
+
<div class="text-sm text-foreground sm:w-2/3">
|
|
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 rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
|
|
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}
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
import SidebarItem from './SidebarItem.svelte';
|
|
13
13
|
|
|
14
14
|
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
15
|
-
import * as Collapsible from './ui/collapsible/index.js';
|
|
16
15
|
import { Avatar } from './ui/avatar/index.js';
|
|
17
16
|
import {
|
|
18
17
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
19
|
-
ChevronLeft, ChevronRight,
|
|
18
|
+
ChevronLeft, ChevronRight, LogOut, Sun, Moon, Palette,
|
|
20
19
|
Image as ImageIcon, Layout, Folder, Type, Video,
|
|
21
|
-
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench
|
|
20
|
+
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench,
|
|
21
|
+
Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Bell, CalendarDays, Package, Shield,
|
|
22
|
+
Barcode, FolderTree, Building2, Warehouse
|
|
22
23
|
} from '@lucide/svelte';
|
|
23
24
|
|
|
24
25
|
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
@@ -63,6 +64,18 @@
|
|
|
63
64
|
'credit-card': CreditCard,
|
|
64
65
|
'book-open': BookOpen,
|
|
65
66
|
'wrench': Wrench,
|
|
67
|
+
'repeat': Repeat,
|
|
68
|
+
'clipboard-check': ClipboardCheck,
|
|
69
|
+
'sliders-horizontal': SlidersHorizontal,
|
|
70
|
+
'alert-triangle': AlertTriangle,
|
|
71
|
+
'bell': Bell,
|
|
72
|
+
'calendar': CalendarDays,
|
|
73
|
+
'package': Package,
|
|
74
|
+
'shield': Shield,
|
|
75
|
+
'barcode': Barcode,
|
|
76
|
+
'folder-tree': FolderTree,
|
|
77
|
+
'building-2': Building2,
|
|
78
|
+
'warehouse': Warehouse,
|
|
66
79
|
};
|
|
67
80
|
|
|
68
81
|
interface NavItem {
|
|
@@ -70,6 +83,7 @@
|
|
|
70
83
|
label: string;
|
|
71
84
|
Icon: typeof LayoutDashboard;
|
|
72
85
|
group?: string;
|
|
86
|
+
badge?: string;
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
interface NavGroup {
|
|
@@ -102,6 +116,7 @@
|
|
|
102
116
|
label: r.label,
|
|
103
117
|
Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
|
|
104
118
|
group: r.group,
|
|
119
|
+
badge: typeof r.meta?.badge === 'string' ? r.meta.badge : undefined,
|
|
105
120
|
});
|
|
106
121
|
}
|
|
107
122
|
}
|
|
@@ -157,8 +172,6 @@
|
|
|
157
172
|
return groups;
|
|
158
173
|
});
|
|
159
174
|
|
|
160
|
-
// Track which groups are open
|
|
161
|
-
let openGroups = $state<Set<string>>(new Set());
|
|
162
175
|
let colorPickerOpen = $state(false);
|
|
163
176
|
let colorPickerRef = $state<HTMLDivElement | null>(null);
|
|
164
177
|
let colorPickerOpenedAt = 0;
|
|
@@ -204,34 +217,42 @@
|
|
|
204
217
|
|
|
205
218
|
<aside
|
|
206
219
|
aria-label="Sidebar navigation"
|
|
207
|
-
class="fixed inset-y-0 left-0 z-30 flex flex-col backdrop-blur-xl transition-all duration-300"
|
|
208
|
-
|
|
209
|
-
class:w-
|
|
210
|
-
|
|
220
|
+
class="fixed inset-y-0 left-0 z-30 flex flex-col border-r border-border/70 bg-sidebar/95 shadow-sm shadow-slate-900/[0.03] backdrop-blur-xl transition-all duration-300"
|
|
221
|
+
class:w-72={!collapsed}
|
|
222
|
+
class:w-20={collapsed}
|
|
223
|
+
data-sidebar="sidebar"
|
|
211
224
|
>
|
|
212
225
|
<!-- Logo -->
|
|
213
|
-
<div class="flex h-16 items-center justify-between px-4">
|
|
226
|
+
<div class="flex h-16 items-center justify-between border-b border-border/60 px-4">
|
|
214
227
|
{#if !collapsed}
|
|
215
228
|
<div class="flex items-center gap-3">
|
|
216
|
-
<div class="w-
|
|
217
|
-
|
|
229
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-sm font-bold text-primary-foreground shadow-sm shadow-primary/25">
|
|
230
|
+
{title.slice(0, 1).toUpperCase()}
|
|
231
|
+
</div>
|
|
232
|
+
<div class="min-w-0">
|
|
233
|
+
<span class="block truncate text-[0.95rem] font-semibold tracking-tight text-sidebar-foreground">{title}</span>
|
|
234
|
+
<span class="block truncate text-[11px] font-medium text-sidebar-foreground/50">{getLocale() === 'zh-CN' ? '管理控制台' : 'Admin Console'}</span>
|
|
235
|
+
</div>
|
|
218
236
|
</div>
|
|
219
237
|
{:else}
|
|
220
|
-
<div class="
|
|
238
|
+
<div class="mx-auto flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-sm font-bold text-primary-foreground shadow-sm shadow-primary/25">
|
|
239
|
+
{title.slice(0, 1).toUpperCase()}
|
|
240
|
+
</div>
|
|
221
241
|
{/if}
|
|
222
242
|
{#if !collapsed}
|
|
223
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
|
|
243
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="rounded-xl text-sidebar-foreground/55 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground">
|
|
224
244
|
<ChevronLeft class="h-4 w-4" />
|
|
225
245
|
</TooltipButton>
|
|
226
246
|
{:else}
|
|
227
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto
|
|
247
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mb-2 mt-2 block rounded-xl text-sidebar-foreground/55 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground">
|
|
228
248
|
<ChevronRight class="h-4 w-4" />
|
|
229
249
|
</TooltipButton>
|
|
230
250
|
{/if}
|
|
231
251
|
</div>
|
|
232
252
|
|
|
233
|
-
<
|
|
234
|
-
<
|
|
253
|
+
<div class="flex min-h-0 flex-1">
|
|
254
|
+
<ScrollArea class="min-w-0 flex-1">
|
|
255
|
+
<nav aria-label="Main menu" class="space-y-5 px-3 py-5">
|
|
235
256
|
{#if menu && menu.length > 0}
|
|
236
257
|
<!-- Custom multi-level menu -->
|
|
237
258
|
{#each menu as item, _i (_i)}
|
|
@@ -241,37 +262,41 @@
|
|
|
241
262
|
<!-- Auto-generated menu from resources (fallback) -->
|
|
242
263
|
{#each navGroups as group, _i (_i)}
|
|
243
264
|
{#if group.name && !collapsed}
|
|
244
|
-
<!-- Grouped section
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}}>
|
|
250
|
-
<Collapsible.Trigger
|
|
251
|
-
class="flex w-full items-center justify-between px-3 py-1.5 text-sm font-semibold tracking-wider text-sidebar-foreground/60 hover:text-sidebar-foreground transition-colors"
|
|
265
|
+
<!-- Grouped section: expanded by default in desktop light sidebar mode -->
|
|
266
|
+
<div>
|
|
267
|
+
<div
|
|
268
|
+
class="flex w-full items-center justify-between px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-sidebar-foreground/45"
|
|
269
|
+
data-sidebar="group-label"
|
|
252
270
|
>
|
|
253
271
|
<span>{group.name}</span>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<Collapsible.Content>
|
|
257
|
-
<div class="mt-1 space-y-1">
|
|
272
|
+
</div>
|
|
273
|
+
<div class="mt-1 space-y-1" data-sidebar="menu-sub">
|
|
258
274
|
{#each group.items as item, _i (_i)}
|
|
259
275
|
{@const active = isActive(item.path)}
|
|
260
276
|
<a
|
|
261
277
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
262
278
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
263
|
-
class="flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-
|
|
279
|
+
class="group relative flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-200
|
|
264
280
|
{active
|
|
265
|
-
? 'bg-
|
|
266
|
-
: 'text-sidebar-foreground/
|
|
281
|
+
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
|
|
282
|
+
: 'text-sidebar-foreground/68 hover:bg-background/85 hover:text-sidebar-foreground'}"
|
|
283
|
+
data-sidebar="menu-sub-button"
|
|
284
|
+
data-active={active ? "true" : "false"}
|
|
267
285
|
>
|
|
268
|
-
|
|
269
|
-
|
|
286
|
+
{#if active && !collapsed}
|
|
287
|
+
<div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
|
|
288
|
+
{/if}
|
|
289
|
+
<span data-sidebar="menu-icon" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg {active ? 'bg-primary-foreground/15' : 'bg-muted/60 text-sidebar-foreground/55 group-hover:bg-primary/10 group-hover:text-primary'}">
|
|
290
|
+
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
291
|
+
</span>
|
|
292
|
+
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
293
|
+
{#if item.badge}
|
|
294
|
+
<span class="rounded-full {active ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-primary/10 text-primary'} px-2 py-0.5 text-[10px] font-semibold">{item.badge}</span>
|
|
295
|
+
{/if}
|
|
270
296
|
</a>
|
|
271
297
|
{/each}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
</Collapsible.Root>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
275
300
|
{:else}
|
|
276
301
|
<!-- Ungrouped items (flat) -->
|
|
277
302
|
{#each group.items as item, _i (_i)}
|
|
@@ -284,10 +309,12 @@
|
|
|
284
309
|
{...props}
|
|
285
310
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
286
311
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
287
|
-
class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-
|
|
312
|
+
class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
|
|
288
313
|
{active
|
|
289
|
-
? 'bg-
|
|
290
|
-
: 'text-sidebar-foreground/60 hover:bg-sidebar-accent/
|
|
314
|
+
? 'bg-primary/10 text-primary shadow-sm shadow-primary/10 ring-1 ring-primary/15'
|
|
315
|
+
: 'text-sidebar-foreground/60 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
|
|
316
|
+
data-sidebar="menu-button"
|
|
317
|
+
data-active={active ? "true" : "false"}
|
|
291
318
|
>
|
|
292
319
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
293
320
|
</a>
|
|
@@ -301,13 +328,23 @@
|
|
|
301
328
|
<a
|
|
302
329
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
303
330
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
304
|
-
class="flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-
|
|
331
|
+
class="group relative flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
|
|
305
332
|
{active
|
|
306
|
-
? 'bg-
|
|
307
|
-
: 'text-sidebar-foreground/
|
|
333
|
+
? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
|
|
334
|
+
: 'text-sidebar-foreground/68 hover:bg-background/85 hover:text-sidebar-foreground'}"
|
|
335
|
+
data-sidebar="menu-button"
|
|
336
|
+
data-active={active ? "true" : "false"}
|
|
308
337
|
>
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
{#if active && !collapsed}
|
|
339
|
+
<div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
|
|
340
|
+
{/if}
|
|
341
|
+
<span data-sidebar="menu-icon" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg {active ? 'bg-primary-foreground/15' : 'bg-muted/60 text-sidebar-foreground/55 group-hover:bg-primary/10 group-hover:text-primary'}">
|
|
342
|
+
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
343
|
+
</span>
|
|
344
|
+
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
345
|
+
{#if item.badge}
|
|
346
|
+
<span class="rounded-full {active ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-primary/10 text-primary'} px-2 py-0.5 text-[10px] font-semibold">{item.badge}</span>
|
|
347
|
+
{/if}
|
|
311
348
|
</a>
|
|
312
349
|
{/if}
|
|
313
350
|
{/each}
|
|
@@ -316,16 +353,17 @@
|
|
|
316
353
|
{/if}
|
|
317
354
|
</nav>
|
|
318
355
|
</ScrollArea>
|
|
356
|
+
</div>
|
|
319
357
|
|
|
320
358
|
<!-- Footer -->
|
|
321
|
-
<div>
|
|
359
|
+
<div class="border-t border-border/60 bg-sidebar/95">
|
|
322
360
|
<!-- Color theme picker -->
|
|
323
361
|
{#if !collapsed}
|
|
324
362
|
<div class="px-3 pt-3 pb-1">
|
|
325
363
|
<div class="relative" bind:this={colorPickerRef}>
|
|
326
364
|
<Button
|
|
327
365
|
variant="ghost"
|
|
328
|
-
class="flex w-full items-center gap-2 rounded-
|
|
366
|
+
class="flex h-auto w-full items-center justify-start gap-2 rounded-xl px-2 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
|
329
367
|
onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
|
|
330
368
|
>
|
|
331
369
|
<Palette class="h-4 w-4" />
|
|
@@ -358,7 +396,7 @@
|
|
|
358
396
|
<!-- User info -->
|
|
359
397
|
{#if !collapsed && identity}
|
|
360
398
|
<div class="px-3 pb-3 pt-1">
|
|
361
|
-
<div class="flex items-center gap-3 rounded-
|
|
399
|
+
<div class="flex items-center gap-3 rounded-2xl border border-border/50 bg-background/65 p-2 shadow-sm shadow-slate-900/[0.02] transition-colors hover:bg-sidebar-accent/50">
|
|
362
400
|
<Avatar
|
|
363
401
|
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
364
402
|
alt={identity.name ?? 'User'}
|
|
@@ -415,6 +453,16 @@
|
|
|
415
453
|
</Tooltip.Trigger>
|
|
416
454
|
<Tooltip.Content side="right">{t('common.toggleTheme')}</Tooltip.Content>
|
|
417
455
|
</Tooltip.Root>
|
|
456
|
+
<Tooltip.Root>
|
|
457
|
+
<Tooltip.Trigger>
|
|
458
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
459
|
+
<Button {...props} variant="ghost" size="icon" onclick={() => navigate('/settings')} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
460
|
+
<Settings class="h-4 w-4" />
|
|
461
|
+
</Button>
|
|
462
|
+
{/snippet}
|
|
463
|
+
</Tooltip.Trigger>
|
|
464
|
+
<Tooltip.Content side="right">{t('settings.title')}</Tooltip.Content>
|
|
465
|
+
</Tooltip.Root>
|
|
418
466
|
<Tooltip.Root>
|
|
419
467
|
<Tooltip.Trigger>
|
|
420
468
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|