@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.
Files changed (70) hide show
  1. package/dist/app.css +372 -0
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/components/AdminApp.svelte +94 -15
  4. package/dist/components/AdminApp.svelte.d.ts +32 -2
  5. package/dist/components/ApiSettings.svelte +191 -0
  6. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  7. package/dist/components/AutoForm.svelte +48 -43
  8. package/dist/components/AutoTable.svelte +27 -22
  9. package/dist/components/ErrorPage.svelte +58 -0
  10. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  11. package/dist/components/FieldRenderer.svelte +2 -2
  12. package/dist/components/ForgotPasswordPage.svelte +111 -68
  13. package/dist/components/Header.svelte +6 -7
  14. package/dist/components/IntegrationsSettings.svelte +70 -0
  15. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  16. package/dist/components/Layout.svelte +8 -8
  17. package/dist/components/LoginPage.svelte +207 -105
  18. package/dist/components/LoginPage.svelte.d.ts +3 -15
  19. package/dist/components/NotificationsSettings.svelte +115 -0
  20. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  21. package/dist/components/PageHeader.svelte +9 -7
  22. package/dist/components/RegisterPage.svelte +103 -65
  23. package/dist/components/ResourceOperationsPage.svelte +153 -0
  24. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  25. package/dist/components/SecuritySettings.svelte +179 -0
  26. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  27. package/dist/components/SettingsPage.svelte +121 -54
  28. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  29. package/dist/components/ShowPage.svelte +9 -9
  30. package/dist/components/Sidebar.svelte +96 -48
  31. package/dist/components/SidebarItem.svelte +37 -12
  32. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  33. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  34. package/dist/components/UpdatePasswordPage.svelte +86 -48
  35. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  36. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  37. package/dist/components/account/GetStartedPage.svelte +123 -0
  38. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  39. package/dist/components/account/ImportMembersPage.svelte +143 -0
  40. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  41. package/dist/components/account/MembersStarterPage.svelte +104 -0
  42. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  43. package/dist/components/account/SecurityLogPage.svelte +108 -0
  44. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  45. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  46. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  47. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  48. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  49. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  50. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  51. package/dist/components/account/TeamMembersPage.svelte +98 -0
  52. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  53. package/dist/components/network/TeamCrewTablePage.svelte +122 -0
  54. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  55. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  57. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  58. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  59. package/dist/components/profile/ProfileCard.svelte +216 -0
  60. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  61. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  62. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  63. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  64. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  65. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  66. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  67. package/dist/index.d.ts +23 -0
  68. package/dist/index.js +27 -0
  69. package/dist/router-state.svelte.js +17 -0
  70. package/package.json +2 -2
@@ -1,66 +1,123 @@
1
1
  <script lang="ts">
2
- import { t } from '@svadmin/core/i18n';
3
- import { navigate } from '@svadmin/core/router';
4
- import { User, Palette, Info, Shield, FileSearch } from '@lucide/svelte';
5
- import ProfilePage from './ProfilePage.svelte';
6
- import AppearanceSettings from './AppearanceSettings.svelte';
7
- import AboutSettings from './AboutSettings.svelte';
8
- import RolesSettings from './RolesSettings.svelte';
9
- import AuditLogViewer from './AuditLogViewer.svelte';
10
- import { getAuthProvider } from '@svadmin/core';
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
- const sections = [
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: 'settings.general',
61
+ group: "settings.general",
17
62
  items: [
18
- { key: 'profile', path: '/settings/profile', icon: User, label: 'settings.profile' },
19
- { key: 'appearance', path: '/settings/appearance', icon: Palette, label: 'settings.appearance' },
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: 'settings.system',
69
+ group: "settings.workspace",
24
70
  items: [
71
+ { key: "security", path: "/settings/security", icon: Lock, label: "settings.security" },
25
72
  ...(authProvider ? [
26
- { key: 'roles', path: '/settings/roles', icon: Shield, label: 'settings.rolesAndPermissions' },
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: 'about', path: '/settings/about', icon: Info, label: 'settings.about' },
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
- import { getParams, getRoute } from '../router-state.svelte.js';
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 === 'appearance') return 'appearance';
39
- if (tab === 'roles') return 'roles';
40
- if (tab === 'audit') return 'audit';
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() === '/settings') {
48
- navigate('/settings/profile');
104
+ if (getRoute() === "/settings") {
105
+ navigate("/settings/profile");
49
106
  }
50
107
  });
51
108
  </script>
52
109
 
53
- <div class="flex flex-col lg:flex-row gap-0 min-h-full">
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 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)]">
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 lg:hidden overflow-x-auto px-4 py-2 gap-1">
58
- {#each sections as section, _i (_i)}
59
- {#each section.items as item, _i (_i)}
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 rounded-md whitespace-nowrap transition-colors
63
- {active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-background/50'}"
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 lg:block py-6 px-3 space-y-6">
75
- <div class="px-3">
76
- <h2 class="text-lg font-semibold text-foreground">{t('settings.title')}</h2>
77
- <p class="text-xs text-muted-foreground mt-1">{t('settings.settingsDescription')}</p>
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 sections as section, _i (_i)}
136
+ {#each resolvedSections as section, _i (_i)}
80
137
  <div>
81
- <h3 class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
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-0.5">
85
- {#each section.items as item, _i (_i)}
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-lg px-3 py-2 text-sm transition-all
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-accent hover:text-accent-foreground'}"
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 === 'profile'}
107
- <ProfilePage />
108
- {:else if activeKey === 'appearance'}
109
- <AppearanceSettings />
110
- {:else if activeKey === 'roles'}
111
- <RolesSettings />
112
- {:else if activeKey === 'audit'}
113
- <AuditLogViewer />
114
- {:else if activeKey === 'about'}
115
- <AboutSettings />
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
- declare const SettingsPage: import("svelte").Component<Record<string, never>, {}, "">;
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/40 shadow-sm">
42
- <Card.Content class="p-0">
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 sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
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/40 shadow-sm">
53
- <Card.Content class="p-0">
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 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">
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/40 shadow-sm">
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, ChevronDown, LogOut, Sun, Moon, Palette,
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
- style="background-color: var(--sidebar);"
209
- class:w-64={!collapsed}
210
- class:w-16={collapsed}
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-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
217
- <span class="font-semibold text-lg tracking-tight text-sidebar-foreground">{title}</span>
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="w-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0 mx-auto" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
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 mt-2 mb-2 block">
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
- <ScrollArea class="flex-1">
234
- <nav aria-label="Main menu" class="py-4 px-3 space-y-1">
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 with Collapsible -->
245
- <Collapsible.Root open={openGroups.has(group.name)} onOpenChange={(isOpen) => {
246
- const next = new Set(openGroups);
247
- if (isOpen) next.add(group.name as string); else next.delete(group.name as string);
248
- openGroups = next;
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
- <ChevronDown class="h-3 w-3 transition-transform {openGroups.has(group.name) ? 'rotate-180' : ''}" />
255
- </Collapsible.Trigger>
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-300
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-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
266
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover: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
- <item.Icon class="h-4 w-4 flex-shrink-0" />
269
- <span>{item.label}</span>
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
- </div>
273
- </Collapsible.Content>
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-300
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-sidebar-accent text-sidebar-accent-foreground shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20'
290
- : 'text-sidebar-foreground/60 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground'}"
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-300
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-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
307
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover: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
- <item.Icon class="h-4 w-4 flex-shrink-0" />
310
- <span>{item.label}</span>
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-lg px-2 py-1.5 text-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground transition-colors h-auto justify-start"
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-lg p-2 hover:bg-sidebar-accent/50 transition-colors">
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> })}