@svadmin/ui 0.34.10 → 0.36.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 +746 -22
- 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 +47 -42
- package/dist/components/AutoTable.svelte +29 -24
- 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 +7 -7
- 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 +151 -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 +120 -87
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +278 -189
- package/dist/components/SidebarItem.svelte +77 -18
- 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 +135 -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 +155 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
- 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,26 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
import { getResources, canAccessAsync } from '@svadmin/core';
|
|
2
|
+
import { getResources, canAccessAsync, getAuthProvider } from '@svadmin/core';
|
|
4
3
|
import type { Identity, MenuItem } from '@svadmin/core';
|
|
5
4
|
import { navigate } from '@svadmin/core/router';
|
|
6
5
|
import { getPath } from '../router-state.svelte.js';
|
|
7
6
|
import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
8
|
-
import { toggleTheme, getResolvedTheme
|
|
9
|
-
import { Button } from './ui/button/index.js';
|
|
7
|
+
import { toggleTheme, getResolvedTheme } from '@svadmin/core';
|
|
10
8
|
import TooltipButton from './TooltipButton.svelte';
|
|
11
9
|
import * as Tooltip from './ui/tooltip/index.js';
|
|
10
|
+
import * as Popover from './ui/popover/index.js';
|
|
12
11
|
import SidebarItem from './SidebarItem.svelte';
|
|
13
12
|
|
|
14
13
|
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
15
|
-
import * as Collapsible from './ui/collapsible/index.js';
|
|
16
14
|
import { Avatar } from './ui/avatar/index.js';
|
|
17
15
|
import {
|
|
18
16
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
19
17
|
ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
|
|
20
18
|
Image as ImageIcon, Layout, Folder, Type, Video,
|
|
21
|
-
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench
|
|
19
|
+
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench,
|
|
20
|
+
Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Bell, CalendarDays, Package, Shield,
|
|
21
|
+
Barcode, FolderTree, Building2, Warehouse, Languages, User, Lock, Puzzle, FileSearch, Info
|
|
22
22
|
} from '@lucide/svelte';
|
|
23
23
|
|
|
24
|
+
const authProvider = getAuthProvider({ optional: true });
|
|
25
|
+
|
|
24
26
|
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
25
27
|
collapsed: boolean;
|
|
26
28
|
identity: Identity | null;
|
|
@@ -38,8 +40,6 @@
|
|
|
38
40
|
: routeMode
|
|
39
41
|
);
|
|
40
42
|
|
|
41
|
-
const _resources = getResources();
|
|
42
|
-
|
|
43
43
|
const iconMap: Record<string, typeof LayoutDashboard> = {
|
|
44
44
|
'dashboard': LayoutDashboard,
|
|
45
45
|
'posts': FileText,
|
|
@@ -63,6 +63,18 @@
|
|
|
63
63
|
'credit-card': CreditCard,
|
|
64
64
|
'book-open': BookOpen,
|
|
65
65
|
'wrench': Wrench,
|
|
66
|
+
'repeat': Repeat,
|
|
67
|
+
'clipboard-check': ClipboardCheck,
|
|
68
|
+
'sliders-horizontal': SlidersHorizontal,
|
|
69
|
+
'alert-triangle': AlertTriangle,
|
|
70
|
+
'bell': Bell,
|
|
71
|
+
'calendar': CalendarDays,
|
|
72
|
+
'package': Package,
|
|
73
|
+
'shield': Shield,
|
|
74
|
+
'barcode': Barcode,
|
|
75
|
+
'folder-tree': FolderTree,
|
|
76
|
+
'building-2': Building2,
|
|
77
|
+
'warehouse': Warehouse,
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
interface NavItem {
|
|
@@ -70,6 +82,7 @@
|
|
|
70
82
|
label: string;
|
|
71
83
|
Icon: typeof LayoutDashboard;
|
|
72
84
|
group?: string;
|
|
85
|
+
badge?: string;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
interface NavGroup {
|
|
@@ -77,11 +90,28 @@
|
|
|
77
90
|
items: NavItem[];
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
interface UserSettingsItem {
|
|
94
|
+
key: string;
|
|
95
|
+
path: string;
|
|
96
|
+
icon: typeof LayoutDashboard;
|
|
97
|
+
label: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const userSettingsItems: UserSettingsItem[] = [
|
|
101
|
+
{ key: 'account', path: '/settings/account', icon: User, label: 'settings.accountPreferences' },
|
|
102
|
+
{ key: 'access', path: '/settings/access', icon: Lock, label: 'settings.accessSecurity' },
|
|
103
|
+
{ key: 'developer', path: '/settings/developer', icon: Puzzle, label: 'settings.integrationApi' },
|
|
104
|
+
...(authProvider ? [
|
|
105
|
+
{ key: 'audit', path: '/settings/audit', icon: FileSearch, label: 'settings.auditLogs' },
|
|
106
|
+
] : []),
|
|
107
|
+
{ key: 'about', path: '/settings/about', icon: Info, label: 'settings.about' },
|
|
108
|
+
];
|
|
109
|
+
|
|
80
110
|
// Use effect to build nav items, taking access control into account
|
|
81
111
|
let navItems = $state.raw<NavItem[]>([]);
|
|
82
112
|
|
|
83
113
|
$effect(() => {
|
|
84
|
-
|
|
114
|
+
getLocale();
|
|
85
115
|
const currentResources = getResources();
|
|
86
116
|
let cancelled = false;
|
|
87
117
|
|
|
@@ -102,6 +132,7 @@
|
|
|
102
132
|
label: r.label,
|
|
103
133
|
Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
|
|
104
134
|
group: r.group,
|
|
135
|
+
badge: typeof r.meta?.badge === 'string' ? r.meta.badge : undefined,
|
|
105
136
|
});
|
|
106
137
|
}
|
|
107
138
|
}
|
|
@@ -125,19 +156,30 @@
|
|
|
125
156
|
|
|
126
157
|
// Track current hash for active state
|
|
127
158
|
const path = $derived(getPath());
|
|
159
|
+
let userMenuOpen = $state(false);
|
|
128
160
|
|
|
129
161
|
function isActive(itemPath: string): boolean {
|
|
130
162
|
if (itemPath === '/') return path === '/';
|
|
131
163
|
return path === itemPath || path.startsWith(itemPath + "/");
|
|
132
164
|
}
|
|
133
165
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
function aggregateBadges(items: { badge?: string }[]): string | undefined {
|
|
167
|
+
const total = items.reduce((sum, item) => {
|
|
168
|
+
const value = Number(item.badge);
|
|
169
|
+
return Number.isFinite(value) ? sum + value : sum;
|
|
170
|
+
}, 0);
|
|
171
|
+
|
|
172
|
+
return total > 0 ? String(total) : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function openUserMenuRoute(path: string) {
|
|
176
|
+
userMenuOpen = false;
|
|
177
|
+
navigate(path);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleUserMenuAction(action: () => void) {
|
|
181
|
+
userMenuOpen = false;
|
|
182
|
+
action();
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
// Group nav items by group field (null = ungrouped)
|
|
@@ -157,11 +199,38 @@
|
|
|
157
199
|
return groups;
|
|
158
200
|
});
|
|
159
201
|
|
|
160
|
-
|
|
161
|
-
let
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
let autoGroupOpen = $state<Record<string, boolean>>({});
|
|
203
|
+
let autoGroupTouched = $state<Record<string, boolean>>({});
|
|
204
|
+
|
|
205
|
+
function isGroupActive(group: NavGroup): boolean {
|
|
206
|
+
return group.items.some((item) => isActive(item.path));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isAutoGroupOpen(group: NavGroup): boolean {
|
|
210
|
+
if (!group.name) return true;
|
|
211
|
+
return autoGroupOpen[group.name] ?? true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function toggleAutoGroup(group: NavGroup) {
|
|
215
|
+
if (!group.name) return;
|
|
216
|
+
const currentOpen = isAutoGroupOpen(group);
|
|
217
|
+
autoGroupTouched = { ...autoGroupTouched, [group.name]: true };
|
|
218
|
+
autoGroupOpen = { ...autoGroupOpen, [group.name]: !currentOpen };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
$effect(() => {
|
|
222
|
+
let nextOpen = autoGroupOpen;
|
|
223
|
+
let changed = false;
|
|
224
|
+
|
|
225
|
+
for (const group of navGroups) {
|
|
226
|
+
if (!group.name || autoGroupTouched[group.name] || !isGroupActive(group) || autoGroupOpen[group.name]) continue;
|
|
227
|
+
if (!changed) nextOpen = { ...autoGroupOpen };
|
|
228
|
+
nextOpen[group.name] = true;
|
|
229
|
+
changed = true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (changed) autoGroupOpen = nextOpen;
|
|
233
|
+
});
|
|
165
234
|
|
|
166
235
|
// Density state
|
|
167
236
|
let density = $state<'compact' | 'standard'>('standard');
|
|
@@ -183,55 +252,49 @@
|
|
|
183
252
|
const pyClass = $derived(density === 'compact' ? 'py-1.5' : 'py-2.5');
|
|
184
253
|
const pyClassGroupItem = $derived(density === 'compact' ? 'py-1.5' : 'py-2');
|
|
185
254
|
|
|
186
|
-
// Click-outside to close color picker
|
|
187
|
-
$effect(() => {
|
|
188
|
-
if (!colorPickerOpen) return;
|
|
189
|
-
function handleMouseDown(e: MouseEvent) {
|
|
190
|
-
// Ignore if opened within last 200ms (same interaction)
|
|
191
|
-
if (Date.now() - colorPickerOpenedAt < 200) return;
|
|
192
|
-
if (colorPickerRef && !colorPickerRef.contains(e.target as Node)) {
|
|
193
|
-
colorPickerOpen = false;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
document.addEventListener('mousedown', handleMouseDown, true);
|
|
197
|
-
return () => {
|
|
198
|
-
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
199
|
-
};
|
|
200
|
-
});
|
|
201
255
|
</script>
|
|
202
256
|
|
|
203
257
|
|
|
204
258
|
|
|
205
259
|
<aside
|
|
206
260
|
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-64={!collapsed}
|
|
261
|
+
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"
|
|
262
|
+
class:w-72={!collapsed}
|
|
210
263
|
class:w-16={collapsed}
|
|
264
|
+
data-sidebar="sidebar"
|
|
265
|
+
data-collapsed={collapsed ? "true" : "false"}
|
|
211
266
|
>
|
|
212
267
|
<!-- Logo -->
|
|
213
|
-
<div class="flex h-16 items-center justify-between px-4">
|
|
268
|
+
<div class="flex border-b border-border/60 {collapsed ? 'h-24 flex-col items-center justify-center gap-1 px-2' : 'h-16 items-center justify-between px-4'}">
|
|
214
269
|
{#if !collapsed}
|
|
215
|
-
<div class="flex items-center gap-3">
|
|
216
|
-
<div class="
|
|
217
|
-
|
|
270
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
271
|
+
<div class="flex h-9 w-6 shrink-0 items-center justify-center text-sm font-bold text-primary">
|
|
272
|
+
{title.slice(0, 1).toUpperCase()}
|
|
273
|
+
</div>
|
|
274
|
+
<div class="min-w-0 flex-1 text-left">
|
|
275
|
+
<span class="block truncate text-[1.15rem] font-semibold leading-6 tracking-tight text-sidebar-foreground">{title}</span>
|
|
276
|
+
<span class="block truncate text-xs font-medium leading-4 text-sidebar-foreground/50">{getLocale() === 'zh-CN' ? '管理控制台' : 'Admin Console'}</span>
|
|
277
|
+
</div>
|
|
218
278
|
</div>
|
|
219
279
|
{:else}
|
|
220
|
-
<div class="
|
|
280
|
+
<div class="mx-auto flex h-9 w-9 shrink-0 items-center justify-center text-sm font-bold text-primary">
|
|
281
|
+
{title.slice(0, 1).toUpperCase()}
|
|
282
|
+
</div>
|
|
221
283
|
{/if}
|
|
222
284
|
{#if !collapsed}
|
|
223
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
|
|
285
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="text-sidebar-foreground/55 hover:bg-transparent hover:text-sidebar-foreground">
|
|
224
286
|
<ChevronLeft class="h-4 w-4" />
|
|
225
287
|
</TooltipButton>
|
|
226
288
|
{:else}
|
|
227
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto
|
|
289
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto text-sidebar-foreground/55 hover:bg-transparent hover:text-sidebar-foreground">
|
|
228
290
|
<ChevronRight class="h-4 w-4" />
|
|
229
291
|
</TooltipButton>
|
|
230
292
|
{/if}
|
|
231
293
|
</div>
|
|
232
294
|
|
|
233
|
-
<
|
|
234
|
-
<
|
|
295
|
+
<div class="flex min-h-0 flex-1">
|
|
296
|
+
<ScrollArea class="min-w-0 flex-1">
|
|
297
|
+
<nav aria-label="Main menu" class="space-y-2 py-4 px-2">
|
|
235
298
|
{#if menu && menu.length > 0}
|
|
236
299
|
<!-- Custom multi-level menu -->
|
|
237
300
|
{#each menu as item, _i (_i)}
|
|
@@ -241,37 +304,51 @@
|
|
|
241
304
|
<!-- Auto-generated menu from resources (fallback) -->
|
|
242
305
|
{#each navGroups as group, _i (_i)}
|
|
243
306
|
{#if group.name && !collapsed}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
307
|
+
{@const groupOpen = isAutoGroupOpen(group)}
|
|
308
|
+
{@const groupActive = isGroupActive(group)}
|
|
309
|
+
{@const groupBadge = aggregateBadges(group.items)}
|
|
310
|
+
<div class="border-b border-border/60 pb-2 last:border-b-0">
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
class="flex w-full items-center justify-between px-0.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] transition-colors {groupActive ? 'text-primary' : 'text-sidebar-foreground/45 hover:text-sidebar-foreground/75'}"
|
|
314
|
+
data-sidebar="group-label"
|
|
315
|
+
aria-expanded={groupOpen}
|
|
316
|
+
onclick={() => toggleAutoGroup(group)}
|
|
252
317
|
>
|
|
253
|
-
<span>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
318
|
+
<span class="flex min-w-0 flex-1 items-center">
|
|
319
|
+
<span class="truncate">{group.name}</span>
|
|
320
|
+
{#if groupBadge}
|
|
321
|
+
<span class="ml-2 rounded-full {groupActive ? 'bg-primary/12 text-primary' : 'bg-sidebar-accent text-sidebar-foreground/60'} px-2 py-0.5 text-[10px] font-semibold tracking-normal">{groupBadge}</span>
|
|
322
|
+
{/if}
|
|
323
|
+
</span>
|
|
324
|
+
<ChevronDown class="h-3.5 w-3.5 shrink-0 transition-transform duration-200 {groupOpen ? 'rotate-180' : ''}" />
|
|
325
|
+
</button>
|
|
326
|
+
{#if groupOpen}
|
|
327
|
+
<div class="mt-0.5 divide-y divide-border/45" data-sidebar="menu-sub">
|
|
258
328
|
{#each group.items as item, _i (_i)}
|
|
259
329
|
{@const active = isActive(item.path)}
|
|
260
330
|
<a
|
|
261
331
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
262
332
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
263
|
-
class="flex items-center gap-3
|
|
333
|
+
class="group relative flex items-center gap-3 px-2 {pyClassGroupItem} text-sm font-medium transition-all duration-200 first:rounded-t-xl last:rounded-b-xl
|
|
264
334
|
{active
|
|
265
|
-
? '
|
|
266
|
-
: 'text-sidebar-foreground/
|
|
335
|
+
? 'text-primary font-semibold'
|
|
336
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
|
|
337
|
+
data-sidebar="menu-sub-button"
|
|
338
|
+
data-active={active ? "true" : "false"}
|
|
267
339
|
>
|
|
268
|
-
<
|
|
269
|
-
|
|
340
|
+
<span data-sidebar="menu-icon" class="flex h-5 w-5 shrink-0 items-center justify-center {active ? 'text-primary' : 'text-sidebar-foreground/55 group-hover:text-primary'}">
|
|
341
|
+
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
342
|
+
</span>
|
|
343
|
+
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
344
|
+
{#if item.badge}
|
|
345
|
+
<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>
|
|
346
|
+
{/if}
|
|
270
347
|
</a>
|
|
271
348
|
{/each}
|
|
272
349
|
</div>
|
|
273
|
-
|
|
274
|
-
</
|
|
350
|
+
{/if}
|
|
351
|
+
</div>
|
|
275
352
|
{:else}
|
|
276
353
|
<!-- Ungrouped items (flat) -->
|
|
277
354
|
{#each group.items as item, _i (_i)}
|
|
@@ -284,10 +361,12 @@
|
|
|
284
361
|
{...props}
|
|
285
362
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
286
363
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
287
|
-
class="flex items-center justify-center
|
|
364
|
+
class="mx-auto flex h-10 w-10 items-center justify-center text-sm font-medium transition-colors duration-200
|
|
288
365
|
{active
|
|
289
|
-
? '
|
|
290
|
-
: 'text-sidebar-foreground/60 hover:
|
|
366
|
+
? 'text-primary'
|
|
367
|
+
: 'text-sidebar-foreground/60 hover:text-sidebar-foreground'}"
|
|
368
|
+
data-sidebar="menu-button"
|
|
369
|
+
data-active={active ? "true" : "false"}
|
|
291
370
|
>
|
|
292
371
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
293
372
|
</a>
|
|
@@ -301,13 +380,19 @@
|
|
|
301
380
|
<a
|
|
302
381
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
303
382
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
304
|
-
class="flex items-center gap-3 rounded-xl px-
|
|
383
|
+
class="group relative flex items-center gap-3 rounded-xl px-2 {pyClass} text-sm font-medium transition-all duration-200
|
|
305
384
|
{active
|
|
306
|
-
? '
|
|
307
|
-
: 'text-sidebar-foreground/
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<span
|
|
385
|
+
? 'text-primary font-semibold'
|
|
386
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
|
|
387
|
+
data-sidebar="menu-button"
|
|
388
|
+
data-active={active ? "true" : "false"}
|
|
389
|
+
> <span data-sidebar="menu-icon" class="flex h-5 w-5 shrink-0 items-center justify-center {active ? 'text-primary' : 'text-sidebar-foreground/55 group-hover:text-primary'}">
|
|
390
|
+
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
391
|
+
</span>
|
|
392
|
+
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
393
|
+
{#if item.badge}
|
|
394
|
+
<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>
|
|
395
|
+
{/if}
|
|
311
396
|
</a>
|
|
312
397
|
{/if}
|
|
313
398
|
{/each}
|
|
@@ -316,129 +401,133 @@
|
|
|
316
401
|
{/if}
|
|
317
402
|
</nav>
|
|
318
403
|
</ScrollArea>
|
|
404
|
+
</div>
|
|
319
405
|
|
|
320
406
|
<!-- Footer -->
|
|
321
|
-
<div>
|
|
322
|
-
<!-- Color theme picker -->
|
|
323
|
-
{#if !collapsed}
|
|
324
|
-
<div class="px-3 pt-3 pb-1">
|
|
325
|
-
<div class="relative" bind:this={colorPickerRef}>
|
|
326
|
-
<Button
|
|
327
|
-
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"
|
|
329
|
-
onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
|
|
330
|
-
>
|
|
331
|
-
<Palette class="h-4 w-4" />
|
|
332
|
-
<span class="flex-1 text-left text-xs">{t('common.toggleTheme')}</span>
|
|
333
|
-
<span
|
|
334
|
-
class="h-3.5 w-3.5 rounded-full ring-1 ring-offset-1 ring-offset-sidebar"
|
|
335
|
-
style="background-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}; --tw-ring-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}"
|
|
336
|
-
></span>
|
|
337
|
-
</Button>
|
|
338
|
-
{#if colorPickerOpen}
|
|
339
|
-
<div class="absolute bottom-full left-0 mb-1 z-50 w-40 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95">
|
|
340
|
-
{#each getColorThemes() as ct, _i (_i)}
|
|
341
|
-
<button
|
|
342
|
-
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
343
|
-
onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
|
|
344
|
-
>
|
|
345
|
-
<span
|
|
346
|
-
class="h-3.5 w-3.5 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
|
|
347
|
-
style="background-color: {ct.color}; {getColorTheme() === ct.id ? `--tw-ring-color: ${ct.color}` : ''}"
|
|
348
|
-
></span>
|
|
349
|
-
<span class="text-xs">{ct.label}</span>
|
|
350
|
-
</button>
|
|
351
|
-
{/each}
|
|
352
|
-
</div>
|
|
353
|
-
{/if}
|
|
354
|
-
</div>
|
|
355
|
-
</div>
|
|
356
|
-
{/if}
|
|
357
|
-
|
|
407
|
+
<div class="border-t border-border/60 bg-sidebar/95">
|
|
358
408
|
<!-- User info -->
|
|
359
409
|
{#if !collapsed && identity}
|
|
360
|
-
<div class="px-3
|
|
361
|
-
<
|
|
362
|
-
<
|
|
363
|
-
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
364
|
-
alt={identity.name ?? 'User'}
|
|
365
|
-
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
366
|
-
size="sm"
|
|
367
|
-
/>
|
|
368
|
-
<div class="flex-1 min-w-0">
|
|
369
|
-
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
370
|
-
<p class="truncate text-[10px] text-sidebar-foreground/50">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
<div class="flex items-center justify-between px-1 mt-1">
|
|
374
|
-
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
375
|
-
<span class="text-xs font-bold">{localeLabel}</span>
|
|
376
|
-
</TooltipButton>
|
|
377
|
-
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
378
|
-
{#if getResolvedTheme() === 'dark'}
|
|
379
|
-
<Sun class="h-4 w-4" />
|
|
380
|
-
{:else}
|
|
381
|
-
<Moon class="h-4 w-4" />
|
|
382
|
-
{/if}
|
|
383
|
-
</TooltipButton>
|
|
384
|
-
<TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
385
|
-
<Settings class="h-4 w-4" />
|
|
386
|
-
</TooltipButton>
|
|
387
|
-
<TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground/60 hover:text-destructive">
|
|
388
|
-
<LogOut class="h-4 w-4" />
|
|
389
|
-
</TooltipButton>
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
{:else if collapsed}
|
|
393
|
-
<div class="px-1 py-3 space-y-1">
|
|
394
|
-
<Tooltip.Root>
|
|
395
|
-
<Tooltip.Trigger>
|
|
410
|
+
<div class="px-3 py-3">
|
|
411
|
+
<Popover.Root bind:open={userMenuOpen}>
|
|
412
|
+
<Popover.Trigger>
|
|
396
413
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
397
|
-
<
|
|
398
|
-
|
|
399
|
-
|
|
414
|
+
<button
|
|
415
|
+
{...props}
|
|
416
|
+
type="button"
|
|
417
|
+
class="flex w-full items-center gap-3 rounded-2xl border border-border/50 bg-background/65 p-2 text-left shadow-sm shadow-slate-900/[0.02] transition-colors hover:bg-sidebar-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
418
|
+
aria-label={identity.name ?? 'User'}
|
|
419
|
+
>
|
|
420
|
+
<Avatar
|
|
421
|
+
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
422
|
+
alt={identity.name ?? 'User'}
|
|
423
|
+
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
424
|
+
size="sm"
|
|
425
|
+
/>
|
|
426
|
+
<div class="min-w-0 flex-1">
|
|
427
|
+
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
428
|
+
<p class="truncate text-[10px] text-sidebar-foreground/50">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
|
|
429
|
+
</div>
|
|
430
|
+
<ChevronDown class="h-3.5 w-3.5 shrink-0 text-sidebar-foreground/45 transition-transform {userMenuOpen ? 'rotate-180' : ''}" />
|
|
431
|
+
</button>
|
|
400
432
|
{/snippet}
|
|
401
|
-
</
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
433
|
+
</Popover.Trigger>
|
|
434
|
+
<Popover.Content align="start" side="right" sideOffset={10} class="w-56 p-2">
|
|
435
|
+
<div class="space-y-1">
|
|
436
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground" onclick={() => handleUserMenuAction(toggleLocale)}>
|
|
437
|
+
<Languages class="h-4 w-4 shrink-0" />
|
|
438
|
+
<span class="min-w-0 flex-1 truncate">{t('common.switchLanguage')}</span>
|
|
439
|
+
<span class="text-xs font-semibold text-muted-foreground">{localeLabel}</span>
|
|
440
|
+
</button>
|
|
441
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground" onclick={() => handleUserMenuAction(toggleTheme)}>
|
|
408
442
|
{#if getResolvedTheme() === 'dark'}
|
|
409
|
-
<Sun class="h-4 w-4" />
|
|
443
|
+
<Sun class="h-4 w-4 shrink-0" />
|
|
410
444
|
{:else}
|
|
411
|
-
<Moon class="h-4 w-4" />
|
|
445
|
+
<Moon class="h-4 w-4 shrink-0" />
|
|
412
446
|
{/if}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
447
|
+
<span class="min-w-0 flex-1 truncate">{t('common.toggleTheme')}</span>
|
|
448
|
+
</button>
|
|
449
|
+
<div class="my-1 h-px bg-border/60"></div>
|
|
450
|
+
{#each userSettingsItems as item, _i (_i)}
|
|
451
|
+
{@const active = isActive(item.path)}
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm transition-colors
|
|
455
|
+
{active
|
|
456
|
+
? 'bg-muted text-foreground font-medium'
|
|
457
|
+
: 'text-muted-foreground hover:bg-muted/70 hover:text-foreground'}"
|
|
458
|
+
onclick={() => openUserMenuRoute(item.path)}
|
|
459
|
+
>
|
|
460
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
461
|
+
<span class="min-w-0 flex-1 truncate">{t(item.label)}</span>
|
|
462
|
+
</button>
|
|
463
|
+
{/each}
|
|
464
|
+
<div class="my-1 h-px bg-border/60"></div>
|
|
465
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive" onclick={() => handleUserMenuAction(onLogout)}>
|
|
466
|
+
<LogOut class="h-4 w-4 shrink-0" />
|
|
467
|
+
<span class="min-w-0 flex-1 truncate">{t('common.logout')}</span>
|
|
468
|
+
</button>
|
|
469
|
+
</div>
|
|
470
|
+
</Popover.Content>
|
|
471
|
+
</Popover.Root>
|
|
428
472
|
</div>
|
|
429
|
-
{:else}
|
|
430
|
-
<div class="flex
|
|
431
|
-
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
473
|
+
{:else if collapsed}
|
|
474
|
+
<div class="flex flex-col items-center px-2 py-3">
|
|
475
|
+
{#if identity}
|
|
476
|
+
<Popover.Root bind:open={userMenuOpen}>
|
|
477
|
+
<Popover.Trigger>
|
|
478
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
479
|
+
<button {...props} type="button" class="mx-auto flex h-10 w-10 items-center justify-center rounded-full text-sidebar-foreground transition-colors hover:bg-sidebar-accent/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50" aria-label={identity.name ?? 'User'}>
|
|
480
|
+
<Avatar
|
|
481
|
+
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
482
|
+
alt={identity.name ?? 'User'}
|
|
483
|
+
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
484
|
+
size="sm"
|
|
485
|
+
/>
|
|
486
|
+
</button>
|
|
487
|
+
{/snippet}
|
|
488
|
+
</Popover.Trigger>
|
|
489
|
+
<Popover.Content align="start" side="right" sideOffset={10} class="w-56 p-2">
|
|
490
|
+
<div class="space-y-1">
|
|
491
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground" onclick={() => handleUserMenuAction(toggleLocale)}>
|
|
492
|
+
<Languages class="h-4 w-4 shrink-0" />
|
|
493
|
+
<span class="min-w-0 flex-1 truncate">{t('common.switchLanguage')}</span>
|
|
494
|
+
<span class="text-xs font-semibold text-muted-foreground">{localeLabel}</span>
|
|
495
|
+
</button>
|
|
496
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-muted/70 hover:text-foreground" onclick={() => handleUserMenuAction(toggleTheme)}>
|
|
497
|
+
{#if getResolvedTheme() === 'dark'}
|
|
498
|
+
<Sun class="h-4 w-4 shrink-0" />
|
|
499
|
+
{:else}
|
|
500
|
+
<Moon class="h-4 w-4 shrink-0" />
|
|
501
|
+
{/if}
|
|
502
|
+
<span class="min-w-0 flex-1 truncate">{t('common.toggleTheme')}</span>
|
|
503
|
+
</button>
|
|
504
|
+
<div class="my-1 h-px bg-border/60"></div>
|
|
505
|
+
{#each userSettingsItems as item, _i (_i)}
|
|
506
|
+
{@const active = isActive(item.path)}
|
|
507
|
+
<button
|
|
508
|
+
type="button"
|
|
509
|
+
class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm transition-colors
|
|
510
|
+
{active
|
|
511
|
+
? 'bg-muted text-foreground font-medium'
|
|
512
|
+
: 'text-muted-foreground hover:bg-muted/70 hover:text-foreground'}"
|
|
513
|
+
onclick={() => openUserMenuRoute(item.path)}
|
|
514
|
+
>
|
|
515
|
+
<item.icon class="h-4 w-4 shrink-0" />
|
|
516
|
+
<span class="min-w-0 flex-1 truncate">{t(item.label)}</span>
|
|
517
|
+
</button>
|
|
518
|
+
{/each}
|
|
519
|
+
<div class="my-1 h-px bg-border/60"></div>
|
|
520
|
+
<button type="button" class="flex w-full items-center gap-3 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive" onclick={() => handleUserMenuAction(onLogout)}>
|
|
521
|
+
<LogOut class="h-4 w-4 shrink-0" />
|
|
522
|
+
<span class="min-w-0 flex-1 truncate">{t('common.logout')}</span>
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
</Popover.Content>
|
|
526
|
+
</Popover.Root>
|
|
527
|
+
{/if}
|
|
441
528
|
</div>
|
|
529
|
+
{:else}
|
|
530
|
+
<div class="h-3"></div>
|
|
442
531
|
{/if}
|
|
443
532
|
</div>
|
|
444
533
|
</aside>
|