@svadmin/ui 0.36.0 → 0.38.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 +76 -104
- package/dist/components/AdminApp.svelte +18 -14
- package/dist/components/AutoForm.svelte +32 -47
- package/dist/components/AutoTable.svelte +26 -31
- package/dist/components/CanAccess.svelte +3 -2
- package/dist/components/CanAccess.svelte.d.ts +1 -0
- package/dist/components/CreateInferencer.svelte +13 -0
- package/dist/components/CreateInferencer.svelte.d.ts +7 -0
- package/dist/components/EditInferencer.svelte +21 -0
- package/dist/components/EditInferencer.svelte.d.ts +9 -0
- package/dist/components/Header.svelte +6 -6
- package/dist/components/Layout.svelte +21 -11
- package/dist/components/ListInferencer.svelte +14 -0
- package/dist/components/ListInferencer.svelte.d.ts +7 -0
- package/dist/components/PageHeader.svelte +7 -9
- package/dist/components/ResourceInferencer.svelte +28 -0
- package/dist/components/ResourceInferencer.svelte.d.ts +7 -0
- package/dist/components/ResourceOperationsPage.svelte +168 -21
- package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -0
- package/dist/components/SettingsPage.svelte +76 -41
- package/dist/components/ShowInferencer.svelte +21 -0
- package/dist/components/ShowInferencer.svelte.d.ts +9 -0
- package/dist/components/ShowPage.svelte +11 -11
- package/dist/components/ShowPage.svelte.d.ts +1 -0
- package/dist/components/Sidebar.svelte +214 -300
- package/dist/components/Sidebar.svelte.d.ts +0 -1
- package/dist/components/SidebarItem.svelte +141 -108
- package/dist/components/account/TeamMembersPage.svelte +9 -3
- package/dist/components/buttons/CloneButton.svelte +10 -2
- package/dist/components/buttons/CloneButton.svelte.d.ts +2 -4
- package/dist/components/buttons/CreateButton.svelte +9 -2
- package/dist/components/buttons/CreateButton.svelte.d.ts +2 -4
- package/dist/components/buttons/DeleteButton.svelte +10 -2
- package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -4
- package/dist/components/buttons/EditButton.svelte +10 -2
- package/dist/components/buttons/EditButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ExportButton.svelte +9 -2
- package/dist/components/buttons/ExportButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ImportButton.svelte +9 -2
- package/dist/components/buttons/ImportButton.svelte.d.ts +2 -4
- package/dist/components/buttons/ShowButton.svelte +10 -2
- package/dist/components/buttons/ShowButton.svelte.d.ts +2 -4
- package/dist/components/buttons/access-control.d.ts +7 -0
- package/dist/components/buttons/access-control.js +5 -0
- package/dist/components/ui/sheet/sheet.svelte +3 -3
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/router-state.svelte.js +15 -0
- package/package.json +2 -2
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getResources, canAccessAsync
|
|
2
|
+
import { getResources, canAccessAsync } from '@svadmin/core';
|
|
3
3
|
import type { Identity, MenuItem } from '@svadmin/core';
|
|
4
4
|
import { navigate } from '@svadmin/core/router';
|
|
5
5
|
import { getPath } from '../router-state.svelte.js';
|
|
6
6
|
import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
7
|
-
import { toggleTheme, getResolvedTheme } from '@svadmin/core';
|
|
7
|
+
import { toggleTheme, getResolvedTheme, getColorThemes, getColorTheme, setColorTheme } from '@svadmin/core';
|
|
8
|
+
import { Button } from './ui/button/index.js';
|
|
8
9
|
import TooltipButton from './TooltipButton.svelte';
|
|
9
10
|
import * as Tooltip from './ui/tooltip/index.js';
|
|
10
|
-
import * as Popover from './ui/popover/index.js';
|
|
11
11
|
import SidebarItem from './SidebarItem.svelte';
|
|
12
12
|
|
|
13
13
|
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
14
|
+
import * as Collapsible from './ui/collapsible/index.js';
|
|
14
15
|
import { Avatar } from './ui/avatar/index.js';
|
|
15
16
|
import {
|
|
16
17
|
LayoutDashboard, FileText, Users, Settings, Home,
|
|
17
18
|
ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
|
|
18
19
|
Image as ImageIcon, Layout, Folder, Type, Video,
|
|
19
20
|
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench,
|
|
20
|
-
Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle,
|
|
21
|
-
|
|
21
|
+
Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Mail, Map as MapIcon, MessageSquare, Send, ShoppingBag, Star, Briefcase, Building2,
|
|
22
|
+
Calendar, Bell, Clock, Tag, Trash2, Shield
|
|
22
23
|
} from '@lucide/svelte';
|
|
23
24
|
|
|
24
|
-
const authProvider = getAuthProvider({ optional: true });
|
|
25
|
-
|
|
26
25
|
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
27
26
|
collapsed: boolean;
|
|
28
27
|
identity: Identity | null;
|
|
@@ -30,7 +29,6 @@
|
|
|
30
29
|
onToggle: () => void;
|
|
31
30
|
onLogout: () => void;
|
|
32
31
|
menu?: MenuItem[];
|
|
33
|
-
/** 'hash' for SPA hash routing, 'path' for SvelteKit/filesystem routing, 'auto' to detect */
|
|
34
32
|
routeMode?: 'hash' | 'path' | 'auto';
|
|
35
33
|
} = $props();
|
|
36
34
|
|
|
@@ -43,6 +41,7 @@
|
|
|
43
41
|
const iconMap: Record<string, typeof LayoutDashboard> = {
|
|
44
42
|
'dashboard': LayoutDashboard,
|
|
45
43
|
'posts': FileText,
|
|
44
|
+
'file': FileText,
|
|
46
45
|
'users': Users,
|
|
47
46
|
'settings': Settings,
|
|
48
47
|
'home': Home,
|
|
@@ -67,14 +66,20 @@
|
|
|
67
66
|
'clipboard-check': ClipboardCheck,
|
|
68
67
|
'sliders-horizontal': SlidersHorizontal,
|
|
69
68
|
'alert-triangle': AlertTriangle,
|
|
69
|
+
'mail': Mail,
|
|
70
|
+
'map': MapIcon,
|
|
71
|
+
'message-square': MessageSquare,
|
|
72
|
+
'send': Send,
|
|
73
|
+
'shopping-bag': ShoppingBag,
|
|
74
|
+
'star': Star,
|
|
75
|
+
'briefcase': Briefcase,
|
|
76
|
+
'building': Building2,
|
|
77
|
+
'calendar': Calendar,
|
|
70
78
|
'bell': Bell,
|
|
71
|
-
'
|
|
72
|
-
'
|
|
79
|
+
'clock': Clock,
|
|
80
|
+
'tag': Tag,
|
|
81
|
+
'trash': Trash2,
|
|
73
82
|
'shield': Shield,
|
|
74
|
-
'barcode': Barcode,
|
|
75
|
-
'folder-tree': FolderTree,
|
|
76
|
-
'building-2': Building2,
|
|
77
|
-
'warehouse': Warehouse,
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
interface NavItem {
|
|
@@ -82,32 +87,13 @@
|
|
|
82
87
|
label: string;
|
|
83
88
|
Icon: typeof LayoutDashboard;
|
|
84
89
|
group?: string;
|
|
85
|
-
badge?: string;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
interface NavGroup {
|
|
89
|
-
name: string | null;
|
|
93
|
+
name: string | null;
|
|
90
94
|
items: NavItem[];
|
|
91
95
|
}
|
|
92
96
|
|
|
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
|
-
|
|
110
|
-
// Use effect to build nav items, taking access control into account
|
|
111
97
|
let navItems = $state.raw<NavItem[]>([]);
|
|
112
98
|
|
|
113
99
|
$effect(() => {
|
|
@@ -132,7 +118,6 @@
|
|
|
132
118
|
label: r.label,
|
|
133
119
|
Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
|
|
134
120
|
group: r.group,
|
|
135
|
-
badge: typeof r.meta?.badge === 'string' ? r.meta.badge : undefined,
|
|
136
121
|
});
|
|
137
122
|
}
|
|
138
123
|
}
|
|
@@ -142,7 +127,6 @@
|
|
|
142
127
|
return () => { cancelled = true; };
|
|
143
128
|
});
|
|
144
129
|
|
|
145
|
-
/** Toggle between available locales */
|
|
146
130
|
function toggleLocale() {
|
|
147
131
|
const locales = getAvailableLocales();
|
|
148
132
|
const current = getLocale();
|
|
@@ -151,38 +135,15 @@
|
|
|
151
135
|
setLocale(next);
|
|
152
136
|
}
|
|
153
137
|
|
|
154
|
-
/** Short display label for current locale */
|
|
155
138
|
const localeLabel = $derived(getLocale() === 'zh-CN' ? '中' : 'EN');
|
|
156
139
|
|
|
157
|
-
// Track current hash for active state
|
|
158
140
|
const path = $derived(getPath());
|
|
159
|
-
let userMenuOpen = $state(false);
|
|
160
141
|
|
|
161
142
|
function isActive(itemPath: string): boolean {
|
|
162
143
|
if (itemPath === '/') return path === '/';
|
|
163
144
|
return path === itemPath || path.startsWith(itemPath + "/");
|
|
164
145
|
}
|
|
165
146
|
|
|
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();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Group nav items by group field (null = ungrouped)
|
|
186
147
|
const navGroups = $derived.by((): NavGroup[] => {
|
|
187
148
|
const groups: NavGroup[] = [];
|
|
188
149
|
const groupMap = new Map<string | null, NavItem[]>();
|
|
@@ -199,40 +160,11 @@
|
|
|
199
160
|
return groups;
|
|
200
161
|
});
|
|
201
162
|
|
|
202
|
-
let
|
|
203
|
-
let
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
});
|
|
163
|
+
let openGroups = $state<Set<string>>(new Set());
|
|
164
|
+
let colorPickerOpen = $state(false);
|
|
165
|
+
let colorPickerRef = $state<HTMLDivElement | null>(null);
|
|
166
|
+
let colorPickerOpenedAt = 0;
|
|
234
167
|
|
|
235
|
-
// Density state
|
|
236
168
|
let density = $state<'compact' | 'standard'>('standard');
|
|
237
169
|
|
|
238
170
|
$effect(() => {
|
|
@@ -249,108 +181,88 @@
|
|
|
249
181
|
}
|
|
250
182
|
});
|
|
251
183
|
|
|
252
|
-
const pyClass = $derived(density === 'compact' ? 'py-1
|
|
253
|
-
const pyClassGroupItem = $derived(density === 'compact' ? 'py-1
|
|
184
|
+
const pyClass = $derived(density === 'compact' ? 'py-1' : 'py-[7px]');
|
|
185
|
+
const pyClassGroupItem = $derived(density === 'compact' ? 'py-1' : 'py-[6px]');
|
|
254
186
|
|
|
187
|
+
$effect(() => {
|
|
188
|
+
if (!colorPickerOpen) return;
|
|
189
|
+
function handleMouseDown(e: MouseEvent) {
|
|
190
|
+
if (Date.now() - colorPickerOpenedAt < 200) return;
|
|
191
|
+
if (colorPickerRef && !colorPickerRef.contains(e.target as Node)) {
|
|
192
|
+
colorPickerOpen = false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
document.addEventListener('mousedown', handleMouseDown, true);
|
|
196
|
+
return () => {
|
|
197
|
+
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
198
|
+
};
|
|
199
|
+
});
|
|
255
200
|
</script>
|
|
256
201
|
|
|
257
202
|
|
|
258
203
|
|
|
259
204
|
<aside
|
|
260
205
|
aria-label="Sidebar navigation"
|
|
261
|
-
class="fixed inset-y-0 left-0 z-30 flex flex-col border-r border-border/
|
|
262
|
-
|
|
263
|
-
class:w-
|
|
264
|
-
|
|
265
|
-
data-collapsed={collapsed ? "true" : "false"}
|
|
206
|
+
class="fixed inset-y-0 left-0 z-30 flex flex-col border-r border-border/40 transition-all duration-300"
|
|
207
|
+
style="background-color: var(--sidebar);"
|
|
208
|
+
class:w-[252px]={!collapsed}
|
|
209
|
+
class:w-[70px]={collapsed}
|
|
266
210
|
>
|
|
267
|
-
|
|
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'}">
|
|
269
|
-
{#if !collapsed}
|
|
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>
|
|
278
|
-
</div>
|
|
279
|
-
{:else}
|
|
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>
|
|
283
|
-
{/if}
|
|
211
|
+
<div class="flex h-[70px] items-center shrink-0" class:px-5={!collapsed} class:justify-center={collapsed}>
|
|
284
212
|
{#if !collapsed}
|
|
285
|
-
<
|
|
286
|
-
<
|
|
287
|
-
|
|
213
|
+
<a href="#/" class="flex items-center gap-2.5 group" onclick={(e) => { e.preventDefault(); navigate('/'); }}>
|
|
214
|
+
<div class="w-[34px] h-[34px] rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0 transition-transform group-hover:scale-105" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
|
|
215
|
+
<span class="font-semibold text-[15px] tracking-[-0.01em] text-sidebar-foreground">{title}</span>
|
|
216
|
+
</a>
|
|
288
217
|
{:else}
|
|
289
|
-
<
|
|
290
|
-
<
|
|
291
|
-
</
|
|
218
|
+
<a href="#/" class="group" aria-label={title} onclick={(e) => { e.preventDefault(); navigate('/'); }}>
|
|
219
|
+
<div class="w-[34px] h-[34px] rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0 transition-transform group-hover:scale-105" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
|
|
220
|
+
</a>
|
|
292
221
|
{/if}
|
|
293
222
|
</div>
|
|
294
223
|
|
|
295
|
-
<
|
|
296
|
-
<
|
|
297
|
-
<nav aria-label="Main menu" class="space-y-2 py-4 px-2">
|
|
224
|
+
<ScrollArea class="flex-1 sidebar-scroll">
|
|
225
|
+
<nav aria-label="Main menu" class="pb-4" class:px-[10px]={!collapsed} class:px-2={collapsed}>
|
|
298
226
|
{#if menu && menu.length > 0}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
227
|
+
<div class="space-y-[2px]">
|
|
228
|
+
{#each menu as item, _i (_i)}
|
|
229
|
+
<SidebarItem {item} currentPath={path} {collapsed} depth={0} />
|
|
230
|
+
{/each}
|
|
231
|
+
</div>
|
|
303
232
|
{:else}
|
|
304
|
-
<!-- Auto-generated menu from resources (fallback) -->
|
|
305
233
|
{#each navGroups as group, _i (_i)}
|
|
306
234
|
{#if group.name && !collapsed}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
class="flex w-full items-center justify-between px-
|
|
314
|
-
data-sidebar="group-label"
|
|
315
|
-
aria-expanded={groupOpen}
|
|
316
|
-
onclick={() => toggleAutoGroup(group)}
|
|
235
|
+
<Collapsible.Root open={openGroups.has(group.name)} onOpenChange={(isOpen) => {
|
|
236
|
+
const next = new Set(openGroups);
|
|
237
|
+
if (isOpen) next.add(group.name as string); else next.delete(group.name as string);
|
|
238
|
+
openGroups = next;
|
|
239
|
+
}}>
|
|
240
|
+
<Collapsible.Trigger
|
|
241
|
+
class="flex w-full items-center justify-between px-[10px] py-[6px] mt-4 mb-[2px] text-[10.5px] font-semibold tracking-[0.06em] uppercase text-sidebar-foreground/45 hover:text-sidebar-foreground/70 transition-colors"
|
|
317
242
|
>
|
|
318
|
-
<span
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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">
|
|
243
|
+
<span>{group.name}</span>
|
|
244
|
+
<ChevronDown class="h-3 w-3 transition-transform duration-200 {openGroups.has(group.name) ? 'rotate-180' : ''}" />
|
|
245
|
+
</Collapsible.Trigger>
|
|
246
|
+
<Collapsible.Content>
|
|
247
|
+
<div class="space-y-[2px]">
|
|
328
248
|
{#each group.items as item, _i (_i)}
|
|
329
249
|
{@const active = isActive(item.path)}
|
|
330
250
|
<a
|
|
331
251
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
332
252
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
333
|
-
class="
|
|
253
|
+
class="sidebar-menu-item flex items-center gap-2.5 rounded-md px-[10px] {pyClassGroupItem} text-[13px] font-medium transition-colors duration-150
|
|
334
254
|
{active
|
|
335
|
-
? 'text-primary
|
|
336
|
-
: 'text-sidebar-foreground/
|
|
337
|
-
data-sidebar="menu-sub-button"
|
|
338
|
-
data-active={active ? "true" : "false"}
|
|
255
|
+
? 'sidebar-menu-item-active bg-sidebar-accent text-primary'
|
|
256
|
+
: 'text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
|
339
257
|
>
|
|
340
|
-
<
|
|
341
|
-
|
|
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}
|
|
258
|
+
<item.Icon class="h-[18px] w-[18px] flex-shrink-0 {active ? 'text-primary' : 'text-sidebar-foreground/50'}" />
|
|
259
|
+
<span>{item.label}</span>
|
|
347
260
|
</a>
|
|
348
261
|
{/each}
|
|
349
262
|
</div>
|
|
350
|
-
|
|
351
|
-
</
|
|
263
|
+
</Collapsible.Content>
|
|
264
|
+
</Collapsible.Root>
|
|
352
265
|
{:else}
|
|
353
|
-
<!-- Ungrouped items (flat) -->
|
|
354
266
|
{#each group.items as item, _i (_i)}
|
|
355
267
|
{@const active = isActive(item.path)}
|
|
356
268
|
{#if collapsed}
|
|
@@ -361,14 +273,12 @@
|
|
|
361
273
|
{...props}
|
|
362
274
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
363
275
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
364
|
-
class="
|
|
276
|
+
class="sidebar-menu-item flex items-center justify-center rounded-md px-2 {pyClass} transition-colors duration-150
|
|
365
277
|
{active
|
|
366
|
-
? 'text-primary'
|
|
367
|
-
: 'text-sidebar-foreground/
|
|
368
|
-
data-sidebar="menu-button"
|
|
369
|
-
data-active={active ? "true" : "false"}
|
|
278
|
+
? 'sidebar-menu-item-active bg-sidebar-accent text-primary'
|
|
279
|
+
: 'text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
370
280
|
>
|
|
371
|
-
<item.Icon class="h-
|
|
281
|
+
<item.Icon class="h-[18px] w-[18px] flex-shrink-0" />
|
|
372
282
|
</a>
|
|
373
283
|
{/snippet}
|
|
374
284
|
</Tooltip.Trigger>
|
|
@@ -380,19 +290,13 @@
|
|
|
380
290
|
<a
|
|
381
291
|
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
382
292
|
onclick={(e) => { e.preventDefault(); navigate(item.path); }}
|
|
383
|
-
class="
|
|
293
|
+
class="sidebar-menu-item flex items-center gap-2.5 rounded-md px-[10px] {pyClass} text-[13px] font-medium transition-colors duration-150
|
|
384
294
|
{active
|
|
385
|
-
? 'text-primary
|
|
386
|
-
: 'text-sidebar-foreground/
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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}
|
|
295
|
+
? 'sidebar-menu-item-active bg-sidebar-accent text-primary'
|
|
296
|
+
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
297
|
+
>
|
|
298
|
+
<item.Icon class="h-[18px] w-[18px] flex-shrink-0 {active ? 'text-primary' : 'text-sidebar-foreground/50'}" />
|
|
299
|
+
<span>{item.label}</span>
|
|
396
300
|
</a>
|
|
397
301
|
{/if}
|
|
398
302
|
{/each}
|
|
@@ -401,133 +305,143 @@
|
|
|
401
305
|
{/if}
|
|
402
306
|
</nav>
|
|
403
307
|
</ScrollArea>
|
|
404
|
-
</div>
|
|
405
308
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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>
|
|
432
|
-
{/snippet}
|
|
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)}>
|
|
442
|
-
{#if getResolvedTheme() === 'dark'}
|
|
443
|
-
<Sun class="h-4 w-4 shrink-0" />
|
|
444
|
-
{:else}
|
|
445
|
-
<Moon class="h-4 w-4 shrink-0" />
|
|
446
|
-
{/if}
|
|
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)}
|
|
309
|
+
<div class="shrink-0 border-t border-border/40">
|
|
310
|
+
{#if !collapsed}
|
|
311
|
+
<div class="px-3 pt-3 pb-1">
|
|
312
|
+
<div class="relative" bind:this={colorPickerRef}>
|
|
313
|
+
<button
|
|
314
|
+
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-[12px] text-sidebar-foreground/60 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground transition-colors"
|
|
315
|
+
onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
|
|
316
|
+
>
|
|
317
|
+
<Palette class="h-3.5 w-3.5" />
|
|
318
|
+
<span class="flex-1 text-left">{t('common.toggleTheme')}</span>
|
|
319
|
+
<span
|
|
320
|
+
class="h-3 w-3 rounded-full ring-1 ring-offset-1 ring-offset-sidebar"
|
|
321
|
+
style="background-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}; --tw-ring-color: {getColorThemes().find(c => c.id === getColorTheme())?.color ?? '#6366f1'}"
|
|
322
|
+
></span>
|
|
323
|
+
</button>
|
|
324
|
+
{#if colorPickerOpen}
|
|
325
|
+
<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">
|
|
326
|
+
{#each getColorThemes() as ct, _i (_i)}
|
|
452
327
|
<button
|
|
453
|
-
|
|
454
|
-
|
|
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)}
|
|
328
|
+
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"
|
|
329
|
+
onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
|
|
459
330
|
>
|
|
460
|
-
<
|
|
461
|
-
|
|
331
|
+
<span
|
|
332
|
+
class="h-3 w-3 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
|
|
333
|
+
style="background-color: {ct.color}; {getColorTheme() === ct.id ? `--tw-ring-color: ${ct.color}` : ''}"
|
|
334
|
+
></span>
|
|
335
|
+
<span class="text-xs">{ct.label}</span>
|
|
462
336
|
</button>
|
|
463
337
|
{/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
338
|
</div>
|
|
470
|
-
|
|
471
|
-
</
|
|
339
|
+
{/if}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
{/if}
|
|
343
|
+
|
|
344
|
+
{#if !collapsed && identity}
|
|
345
|
+
<div class="px-3 pb-3 pt-1">
|
|
346
|
+
<div class="flex items-center gap-2.5 rounded-lg p-2 transition-colors hover:bg-sidebar-accent/40 cursor-pointer">
|
|
347
|
+
<Avatar
|
|
348
|
+
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
349
|
+
alt={identity.name ?? 'User'}
|
|
350
|
+
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
351
|
+
size="sm"
|
|
352
|
+
/>
|
|
353
|
+
<div class="flex-1 min-w-0">
|
|
354
|
+
<p class="truncate text-[13px] font-medium text-sidebar-foreground">{identity.name}</p>
|
|
355
|
+
<p class="truncate text-[11px] text-sidebar-foreground/45">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
|
|
356
|
+
</div>
|
|
357
|
+
<button
|
|
358
|
+
class="h-7 w-7 flex items-center justify-center rounded-md text-sidebar-foreground/40 hover:text-sidebar-foreground hover:bg-sidebar-accent/60 transition-colors"
|
|
359
|
+
onclick={onLogout}
|
|
360
|
+
>
|
|
361
|
+
<LogOut class="h-3.5 w-3.5" />
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="mt-1 flex items-center justify-between px-1">
|
|
365
|
+
<div class="flex items-center gap-0.5">
|
|
366
|
+
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
367
|
+
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
368
|
+
</TooltipButton>
|
|
369
|
+
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
370
|
+
{#if getResolvedTheme() === 'dark'}
|
|
371
|
+
<Sun class="h-3.5 w-3.5" />
|
|
372
|
+
{:else}
|
|
373
|
+
<Moon class="h-3.5 w-3.5" />
|
|
374
|
+
{/if}
|
|
375
|
+
</TooltipButton>
|
|
376
|
+
<TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
377
|
+
<Settings class="h-3.5 w-3.5" />
|
|
378
|
+
</TooltipButton>
|
|
379
|
+
</div>
|
|
380
|
+
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
381
|
+
<ChevronLeft class="h-3.5 w-3.5" />
|
|
382
|
+
</TooltipButton>
|
|
383
|
+
</div>
|
|
472
384
|
</div>
|
|
473
385
|
{:else if collapsed}
|
|
474
|
-
<div class="flex flex-col items-center px-2 py-3">
|
|
475
|
-
|
|
476
|
-
<
|
|
477
|
-
<
|
|
478
|
-
|
|
479
|
-
<
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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}
|
|
386
|
+
<div class="flex flex-col items-center gap-0.5 px-2 py-3">
|
|
387
|
+
<Tooltip.Root>
|
|
388
|
+
<Tooltip.Trigger>
|
|
389
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
390
|
+
<Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
391
|
+
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
392
|
+
</Button>
|
|
393
|
+
{/snippet}
|
|
394
|
+
</Tooltip.Trigger>
|
|
395
|
+
<Tooltip.Content side="right">{t('common.switchLanguage')}</Tooltip.Content>
|
|
396
|
+
</Tooltip.Root>
|
|
397
|
+
<Tooltip.Root>
|
|
398
|
+
<Tooltip.Trigger>
|
|
399
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
400
|
+
<Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
401
|
+
{#if getResolvedTheme() === 'dark'}
|
|
402
|
+
<Sun class="h-3.5 w-3.5" />
|
|
403
|
+
{:else}
|
|
404
|
+
<Moon class="h-3.5 w-3.5" />
|
|
405
|
+
{/if}
|
|
406
|
+
</Button>
|
|
407
|
+
{/snippet}
|
|
408
|
+
</Tooltip.Trigger>
|
|
409
|
+
<Tooltip.Content side="right">{t('common.toggleTheme')}</Tooltip.Content>
|
|
410
|
+
</Tooltip.Root>
|
|
411
|
+
<Tooltip.Root>
|
|
412
|
+
<Tooltip.Trigger>
|
|
413
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
414
|
+
<Button {...props} variant="ghost" size="icon" onclick={onToggle} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
415
|
+
<ChevronRight class="h-3.5 w-3.5" />
|
|
416
|
+
</Button>
|
|
417
|
+
{/snippet}
|
|
418
|
+
</Tooltip.Trigger>
|
|
419
|
+
<Tooltip.Content side="right">{t('common.toggleSidebar')}</Tooltip.Content>
|
|
420
|
+
</Tooltip.Root>
|
|
421
|
+
<Tooltip.Root>
|
|
422
|
+
<Tooltip.Trigger>
|
|
423
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
424
|
+
<Button {...props} variant="ghost" size="icon" onclick={onLogout} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-destructive">
|
|
425
|
+
<LogOut class="h-3.5 w-3.5" />
|
|
426
|
+
</Button>
|
|
427
|
+
{/snippet}
|
|
428
|
+
</Tooltip.Trigger>
|
|
429
|
+
<Tooltip.Content side="right">{t('common.logout')}</Tooltip.Content>
|
|
430
|
+
</Tooltip.Root>
|
|
528
431
|
</div>
|
|
529
432
|
{:else}
|
|
530
|
-
<div class="
|
|
433
|
+
<div class="flex justify-center gap-1 p-3">
|
|
434
|
+
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
435
|
+
<span class="text-[11px] font-semibold">{localeLabel}</span>
|
|
436
|
+
</TooltipButton>
|
|
437
|
+
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="h-8 w-8 rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground">
|
|
438
|
+
{#if getResolvedTheme() === 'dark'}
|
|
439
|
+
<Sun class="h-3.5 w-3.5" />
|
|
440
|
+
{:else}
|
|
441
|
+
<Moon class="h-3.5 w-3.5" />
|
|
442
|
+
{/if}
|
|
443
|
+
</TooltipButton>
|
|
444
|
+
</div>
|
|
531
445
|
{/if}
|
|
532
446
|
</div>
|
|
533
447
|
</aside>
|