@svadmin/ui 0.35.0 → 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 +412 -60
- package/dist/components/AutoForm.svelte +8 -8
- package/dist/components/AutoTable.svelte +2 -2
- package/dist/components/ErrorPage.svelte +1 -1
- package/dist/components/Layout.svelte +1 -1
- package/dist/components/PageHeader.svelte +3 -3
- package/dist/components/ResourceOperationsPage.svelte +12 -14
- package/dist/components/SettingsPage.svelte +41 -75
- package/dist/components/ShowPage.svelte +5 -5
- package/dist/components/Sidebar.svelte +233 -192
- package/dist/components/SidebarItem.svelte +53 -19
- package/dist/components/TwoFactorAuthPage.svelte +1 -1
- package/dist/components/account/CompanyProfilePage.svelte +1 -1
- package/dist/components/account/GetStartedPage.svelte +1 -1
- package/dist/components/account/ImportMembersPage.svelte +1 -1
- package/dist/components/account/MembersStarterPage.svelte +1 -1
- package/dist/components/account/SecurityLogPage.svelte +29 -2
- package/dist/components/account/SettingsEnterprisePage.svelte +1 -1
- package/dist/components/account/SettingsPlainPage.svelte +1 -1
- package/dist/components/account/SettingsSidebarPage.svelte +1 -1
- package/dist/components/account/TeamMembersPage.svelte +1 -1
- package/dist/components/network/TeamCrewTablePage.svelte +35 -2
- package/dist/components/network/UserCardsNFTPage.svelte +1 -1
- package/dist/components/profile/PublicProfilePage.svelte +1 -1
- package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
- package/package.json +2 -2
|
@@ -1,27 +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
14
|
import { Avatar } from './ui/avatar/index.js';
|
|
16
15
|
import {
|
|
17
16
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
18
|
-
ChevronLeft, ChevronRight, LogOut, Sun, Moon, Palette,
|
|
17
|
+
ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
|
|
19
18
|
Image as ImageIcon, Layout, Folder, Type, Video,
|
|
20
19
|
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench,
|
|
21
20
|
Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Bell, CalendarDays, Package, Shield,
|
|
22
|
-
Barcode, FolderTree, Building2, Warehouse
|
|
21
|
+
Barcode, FolderTree, Building2, Warehouse, Languages, User, Lock, Puzzle, FileSearch, Info
|
|
23
22
|
} from '@lucide/svelte';
|
|
24
23
|
|
|
24
|
+
const authProvider = getAuthProvider({ optional: true });
|
|
25
|
+
|
|
25
26
|
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
26
27
|
collapsed: boolean;
|
|
27
28
|
identity: Identity | null;
|
|
@@ -39,8 +40,6 @@
|
|
|
39
40
|
: routeMode
|
|
40
41
|
);
|
|
41
42
|
|
|
42
|
-
const _resources = getResources();
|
|
43
|
-
|
|
44
43
|
const iconMap: Record<string, typeof LayoutDashboard> = {
|
|
45
44
|
'dashboard': LayoutDashboard,
|
|
46
45
|
'posts': FileText,
|
|
@@ -91,11 +90,28 @@
|
|
|
91
90
|
items: NavItem[];
|
|
92
91
|
}
|
|
93
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
|
+
|
|
94
110
|
// Use effect to build nav items, taking access control into account
|
|
95
111
|
let navItems = $state.raw<NavItem[]>([]);
|
|
96
112
|
|
|
97
113
|
$effect(() => {
|
|
98
|
-
|
|
114
|
+
getLocale();
|
|
99
115
|
const currentResources = getResources();
|
|
100
116
|
let cancelled = false;
|
|
101
117
|
|
|
@@ -140,19 +156,30 @@
|
|
|
140
156
|
|
|
141
157
|
// Track current hash for active state
|
|
142
158
|
const path = $derived(getPath());
|
|
159
|
+
let userMenuOpen = $state(false);
|
|
143
160
|
|
|
144
161
|
function isActive(itemPath: string): boolean {
|
|
145
162
|
if (itemPath === '/') return path === '/';
|
|
146
163
|
return path === itemPath || path.startsWith(itemPath + "/");
|
|
147
164
|
}
|
|
148
165
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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();
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
// Group nav items by group field (null = ungrouped)
|
|
@@ -172,9 +199,38 @@
|
|
|
172
199
|
return groups;
|
|
173
200
|
});
|
|
174
201
|
|
|
175
|
-
let
|
|
176
|
-
let
|
|
177
|
-
|
|
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
|
+
});
|
|
178
234
|
|
|
179
235
|
// Density state
|
|
180
236
|
let density = $state<'compact' | 'standard'>('standard');
|
|
@@ -196,21 +252,6 @@
|
|
|
196
252
|
const pyClass = $derived(density === 'compact' ? 'py-1.5' : 'py-2.5');
|
|
197
253
|
const pyClassGroupItem = $derived(density === 'compact' ? 'py-1.5' : 'py-2');
|
|
198
254
|
|
|
199
|
-
// Click-outside to close color picker
|
|
200
|
-
$effect(() => {
|
|
201
|
-
if (!colorPickerOpen) return;
|
|
202
|
-
function handleMouseDown(e: MouseEvent) {
|
|
203
|
-
// Ignore if opened within last 200ms (same interaction)
|
|
204
|
-
if (Date.now() - colorPickerOpenedAt < 200) return;
|
|
205
|
-
if (colorPickerRef && !colorPickerRef.contains(e.target as Node)) {
|
|
206
|
-
colorPickerOpen = false;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
document.addEventListener('mousedown', handleMouseDown, true);
|
|
210
|
-
return () => {
|
|
211
|
-
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
212
|
-
};
|
|
213
|
-
});
|
|
214
255
|
</script>
|
|
215
256
|
|
|
216
257
|
|
|
@@ -219,32 +260,33 @@
|
|
|
219
260
|
aria-label="Sidebar navigation"
|
|
220
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"
|
|
221
262
|
class:w-72={!collapsed}
|
|
222
|
-
class:w-
|
|
263
|
+
class:w-16={collapsed}
|
|
223
264
|
data-sidebar="sidebar"
|
|
265
|
+
data-collapsed={collapsed ? "true" : "false"}
|
|
224
266
|
>
|
|
225
267
|
<!-- Logo -->
|
|
226
|
-
<div class="flex h-
|
|
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'}">
|
|
227
269
|
{#if !collapsed}
|
|
228
|
-
<div class="flex items-center gap-3">
|
|
229
|
-
<div class="flex h-9 w-
|
|
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">
|
|
230
272
|
{title.slice(0, 1).toUpperCase()}
|
|
231
273
|
</div>
|
|
232
|
-
<div class="min-w-0">
|
|
233
|
-
<span class="block truncate text-[
|
|
234
|
-
<span class="block truncate text-
|
|
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>
|
|
235
277
|
</div>
|
|
236
278
|
</div>
|
|
237
279
|
{:else}
|
|
238
|
-
<div class="mx-auto flex h-9 w-9 shrink-0 items-center justify-center
|
|
280
|
+
<div class="mx-auto flex h-9 w-9 shrink-0 items-center justify-center text-sm font-bold text-primary">
|
|
239
281
|
{title.slice(0, 1).toUpperCase()}
|
|
240
282
|
</div>
|
|
241
283
|
{/if}
|
|
242
284
|
{#if !collapsed}
|
|
243
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="
|
|
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">
|
|
244
286
|
<ChevronLeft class="h-4 w-4" />
|
|
245
287
|
</TooltipButton>
|
|
246
288
|
{:else}
|
|
247
|
-
<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">
|
|
248
290
|
<ChevronRight class="h-4 w-4" />
|
|
249
291
|
</TooltipButton>
|
|
250
292
|
{/if}
|
|
@@ -252,7 +294,7 @@
|
|
|
252
294
|
|
|
253
295
|
<div class="flex min-h-0 flex-1">
|
|
254
296
|
<ScrollArea class="min-w-0 flex-1">
|
|
255
|
-
<nav aria-label="Main menu" class="space-y-
|
|
297
|
+
<nav aria-label="Main menu" class="space-y-2 py-4 px-2">
|
|
256
298
|
{#if menu && menu.length > 0}
|
|
257
299
|
<!-- Custom multi-level menu -->
|
|
258
300
|
{#each menu as item, _i (_i)}
|
|
@@ -262,31 +304,40 @@
|
|
|
262
304
|
<!-- Auto-generated menu from resources (fallback) -->
|
|
263
305
|
{#each navGroups as group, _i (_i)}
|
|
264
306
|
{#if group.name && !collapsed}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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'}"
|
|
269
314
|
data-sidebar="group-label"
|
|
315
|
+
aria-expanded={groupOpen}
|
|
316
|
+
onclick={() => toggleAutoGroup(group)}
|
|
270
317
|
>
|
|
271
|
-
<span>
|
|
272
|
-
|
|
273
|
-
|
|
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">
|
|
274
328
|
{#each group.items as item, _i (_i)}
|
|
275
329
|
{@const active = isActive(item.path)}
|
|
276
330
|
<a
|
|
277
331
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
278
332
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
279
|
-
class="group relative 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
|
|
280
334
|
{active
|
|
281
|
-
? '
|
|
282
|
-
: 'text-sidebar-foreground/68 hover:bg-
|
|
335
|
+
? 'text-primary font-semibold'
|
|
336
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
|
|
283
337
|
data-sidebar="menu-sub-button"
|
|
284
338
|
data-active={active ? "true" : "false"}
|
|
285
339
|
>
|
|
286
|
-
{
|
|
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'}">
|
|
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'}">
|
|
290
341
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
291
342
|
</span>
|
|
292
343
|
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
@@ -295,7 +346,8 @@
|
|
|
295
346
|
{/if}
|
|
296
347
|
</a>
|
|
297
348
|
{/each}
|
|
298
|
-
|
|
349
|
+
</div>
|
|
350
|
+
{/if}
|
|
299
351
|
</div>
|
|
300
352
|
{:else}
|
|
301
353
|
<!-- Ungrouped items (flat) -->
|
|
@@ -309,10 +361,10 @@
|
|
|
309
361
|
{...props}
|
|
310
362
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
311
363
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
312
|
-
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
|
|
313
365
|
{active
|
|
314
|
-
? '
|
|
315
|
-
: 'text-sidebar-foreground/60 hover:
|
|
366
|
+
? 'text-primary'
|
|
367
|
+
: 'text-sidebar-foreground/60 hover:text-sidebar-foreground'}"
|
|
316
368
|
data-sidebar="menu-button"
|
|
317
369
|
data-active={active ? "true" : "false"}
|
|
318
370
|
>
|
|
@@ -328,17 +380,13 @@
|
|
|
328
380
|
<a
|
|
329
381
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
330
382
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
331
|
-
class="group relative 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
|
|
332
384
|
{active
|
|
333
|
-
? '
|
|
334
|
-
: 'text-sidebar-foreground/68 hover:bg-
|
|
385
|
+
? 'text-primary font-semibold'
|
|
386
|
+
: 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
|
|
335
387
|
data-sidebar="menu-button"
|
|
336
388
|
data-active={active ? "true" : "false"}
|
|
337
|
-
>
|
|
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'}">
|
|
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'}">
|
|
342
390
|
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
343
391
|
</span>
|
|
344
392
|
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
@@ -357,136 +405,129 @@
|
|
|
357
405
|
|
|
358
406
|
<!-- Footer -->
|
|
359
407
|
<div class="border-t border-border/60 bg-sidebar/95">
|
|
360
|
-
<!-- Color theme picker -->
|
|
361
|
-
{#if !collapsed}
|
|
362
|
-
<div class="px-3 pt-3 pb-1">
|
|
363
|
-
<div class="relative" bind:this={colorPickerRef}>
|
|
364
|
-
<Button
|
|
365
|
-
variant="ghost"
|
|
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"
|
|
367
|
-
onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
|
|
368
|
-
>
|
|
369
|
-
<Palette class="h-4 w-4" />
|
|
370
|
-
<span class="flex-1 text-left text-xs">{t('common.toggleTheme')}</span>
|
|
371
|
-
<span
|
|
372
|
-
class="h-3.5 w-3.5 rounded-full ring-1 ring-offset-1 ring-offset-sidebar"
|
|
373
|
-
style="background-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}; --tw-ring-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}"
|
|
374
|
-
></span>
|
|
375
|
-
</Button>
|
|
376
|
-
{#if colorPickerOpen}
|
|
377
|
-
<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">
|
|
378
|
-
{#each getColorThemes() as ct, _i (_i)}
|
|
379
|
-
<button
|
|
380
|
-
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"
|
|
381
|
-
onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
|
|
382
|
-
>
|
|
383
|
-
<span
|
|
384
|
-
class="h-3.5 w-3.5 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
|
|
385
|
-
style="background-color: {ct.color}; {getColorTheme() === ct.id ? `--tw-ring-color: ${ct.color}` : ''}"
|
|
386
|
-
></span>
|
|
387
|
-
<span class="text-xs">{ct.label}</span>
|
|
388
|
-
</button>
|
|
389
|
-
{/each}
|
|
390
|
-
</div>
|
|
391
|
-
{/if}
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
{/if}
|
|
395
|
-
|
|
396
408
|
<!-- User info -->
|
|
397
409
|
{#if !collapsed && identity}
|
|
398
|
-
<div class="px-3
|
|
399
|
-
<
|
|
400
|
-
<
|
|
401
|
-
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
402
|
-
alt={identity.name ?? 'User'}
|
|
403
|
-
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
404
|
-
size="sm"
|
|
405
|
-
/>
|
|
406
|
-
<div class="flex-1 min-w-0">
|
|
407
|
-
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
408
|
-
<p class="truncate text-[10px] text-sidebar-foreground/50">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
<div class="flex items-center justify-between px-1 mt-1">
|
|
412
|
-
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
413
|
-
<span class="text-xs font-bold">{localeLabel}</span>
|
|
414
|
-
</TooltipButton>
|
|
415
|
-
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
416
|
-
{#if getResolvedTheme() === 'dark'}
|
|
417
|
-
<Sun class="h-4 w-4" />
|
|
418
|
-
{:else}
|
|
419
|
-
<Moon class="h-4 w-4" />
|
|
420
|
-
{/if}
|
|
421
|
-
</TooltipButton>
|
|
422
|
-
<TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
423
|
-
<Settings class="h-4 w-4" />
|
|
424
|
-
</TooltipButton>
|
|
425
|
-
<TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground/60 hover:text-destructive">
|
|
426
|
-
<LogOut class="h-4 w-4" />
|
|
427
|
-
</TooltipButton>
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
{:else if collapsed}
|
|
431
|
-
<div class="px-1 py-3 space-y-1">
|
|
432
|
-
<Tooltip.Root>
|
|
433
|
-
<Tooltip.Trigger>
|
|
410
|
+
<div class="px-3 py-3">
|
|
411
|
+
<Popover.Root bind:open={userMenuOpen}>
|
|
412
|
+
<Popover.Trigger>
|
|
434
413
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
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>
|
|
438
432
|
{/snippet}
|
|
439
|
-
</
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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)}>
|
|
446
442
|
{#if getResolvedTheme() === 'dark'}
|
|
447
|
-
<Sun class="h-4 w-4" />
|
|
443
|
+
<Sun class="h-4 w-4 shrink-0" />
|
|
448
444
|
{:else}
|
|
449
|
-
<Moon class="h-4 w-4" />
|
|
445
|
+
<Moon class="h-4 w-4 shrink-0" />
|
|
450
446
|
{/if}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<
|
|
470
|
-
<LogOut class="h-4 w-4" />
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
</
|
|
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>
|
|
476
472
|
</div>
|
|
477
|
-
{:else}
|
|
478
|
-
<div class="flex
|
|
479
|
-
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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}
|
|
489
528
|
</div>
|
|
529
|
+
{:else}
|
|
530
|
+
<div class="h-3"></div>
|
|
490
531
|
{/if}
|
|
491
532
|
</div>
|
|
492
533
|
</aside>
|