@svadmin/ui 0.29.1 → 0.29.2
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/package.json +20 -9
- package/src/README.md +0 -18
- package/src/actions.ts +0 -110
- package/src/app.css +0 -673
- package/src/component-registry.svelte.ts +0 -60
- package/src/components/AICommandBar.svelte +0 -180
- package/src/components/AboutSettings.svelte +0 -106
- package/src/components/AdminApp.svelte +0 -199
- package/src/components/AnimatedCounter.svelte +0 -46
- package/src/components/AnomalyBadge.svelte +0 -65
- package/src/components/AppearanceSettings.svelte +0 -183
- package/src/components/AuditLogDrawer.svelte +0 -93
- package/src/components/AuditLogViewer.svelte +0 -226
- package/src/components/Authenticated.svelte +0 -51
- package/src/components/AutoForm.svelte +0 -242
- package/src/components/AutoSaveIndicator.svelte +0 -35
- package/src/components/AutoTable.svelte +0 -839
- package/src/components/Breadcrumbs.svelte +0 -58
- package/src/components/CanAccess.svelte +0 -23
- package/src/components/CatchAllNavigate.svelte +0 -21
- package/src/components/ChatDialog.svelte +0 -535
- package/src/components/ComboboxField.svelte +0 -129
- package/src/components/CommandPalette.svelte +0 -135
- package/src/components/ConfigErrorScreen.svelte +0 -126
- package/src/components/ConfirmDialog.svelte +0 -53
- package/src/components/CopilotPanel.svelte +0 -163
- package/src/components/CreatePage.svelte +0 -39
- package/src/components/DevTools.svelte +0 -149
- package/src/components/DraggableGrid.svelte +0 -86
- package/src/components/DraggableHeader.svelte +0 -112
- package/src/components/DrawerForm.svelte +0 -35
- package/src/components/EditPage.svelte +0 -52
- package/src/components/EmptyState.svelte +0 -31
- package/src/components/ErrorBoundary.svelte +0 -49
- package/src/components/ErrorComponent.svelte +0 -36
- package/src/components/FieldRenderer.svelte +0 -357
- package/src/components/ForgotPasswordPage.svelte +0 -112
- package/src/components/Header.svelte +0 -94
- package/src/components/InferencerPanel.svelte +0 -192
- package/src/components/InfiniteList.svelte +0 -93
- package/src/components/InlineEdit.svelte +0 -88
- package/src/components/InsightCard.svelte +0 -98
- package/src/components/KeyboardShortcuts.svelte +0 -61
- package/src/components/Layout.svelte +0 -167
- package/src/components/ListPage.svelte +0 -71
- package/src/components/LiveIndicator.svelte +0 -58
- package/src/components/LoginPage.svelte +0 -163
- package/src/components/MarkdownRenderer.svelte +0 -127
- package/src/components/ModalForm.svelte +0 -38
- package/src/components/NavigateToResource.svelte +0 -20
- package/src/components/PageHeader.svelte +0 -39
- package/src/components/PageSkeleton.svelte +0 -71
- package/src/components/PasswordInput.svelte +0 -96
- package/src/components/PermissionMatrix.svelte +0 -185
- package/src/components/ProfilePage.svelte +0 -284
- package/src/components/RegisterPage.svelte +0 -112
- package/src/components/RolesSettings.svelte +0 -146
- package/src/components/SettingsPage.svelte +0 -113
- package/src/components/ShowPage.svelte +0 -73
- package/src/components/Sidebar.svelte +0 -417
- package/src/components/SidebarItem.svelte +0 -129
- package/src/components/SmartSuggest.svelte +0 -158
- package/src/components/StatsCard.svelte +0 -79
- package/src/components/StepsForm.svelte +0 -192
- package/src/components/TaskQueueDrawer.svelte +0 -158
- package/src/components/TenantSwitcher.svelte +0 -84
- package/src/components/ThemedTitle.svelte +0 -32
- package/src/components/Toast.svelte +0 -46
- package/src/components/TooltipButton.svelte +0 -42
- package/src/components/UndoableNotification.svelte +0 -69
- package/src/components/UpdatePasswordPage.svelte +0 -89
- package/src/components/VirtualTable.svelte +0 -76
- package/src/components/VoiceInput.svelte +0 -120
- package/src/components/buttons/CloneButton.svelte +0 -30
- package/src/components/buttons/CreateButton.svelte +0 -29
- package/src/components/buttons/DeleteButton.svelte +0 -59
- package/src/components/buttons/EditButton.svelte +0 -30
- package/src/components/buttons/ExportButton.svelte +0 -30
- package/src/components/buttons/ImportButton.svelte +0 -56
- package/src/components/buttons/ListButton.svelte +0 -23
- package/src/components/buttons/RefreshButton.svelte +0 -36
- package/src/components/buttons/SaveButton.svelte +0 -27
- package/src/components/buttons/ShowButton.svelte +0 -31
- package/src/components/buttons/index.ts +0 -10
- package/src/components/charts/BarChart.svelte +0 -69
- package/src/components/charts/LineChart.svelte +0 -115
- package/src/components/charts/PieChart.svelte +0 -95
- package/src/components/charts/index.ts +0 -3
- package/src/components/fieldComponentMap.ts +0 -79
- package/src/components/fields/ArrayField.svelte +0 -86
- package/src/components/fields/BooleanField.svelte +0 -13
- package/src/components/fields/DateField.svelte +0 -6
- package/src/components/fields/EmailField.svelte +0 -9
- package/src/components/fields/FileField.svelte +0 -14
- package/src/components/fields/ImageField.svelte +0 -9
- package/src/components/fields/JsonField.svelte +0 -72
- package/src/components/fields/MarkdownField.svelte +0 -9
- package/src/components/fields/MultiSelectField.svelte +0 -21
- package/src/components/fields/NumberField.svelte +0 -6
- package/src/components/fields/RelationField.svelte +0 -24
- package/src/components/fields/RichTextField.svelte +0 -35
- package/src/components/fields/SelectField.svelte +0 -14
- package/src/components/fields/TagField.svelte +0 -12
- package/src/components/fields/TextField.svelte +0 -11
- package/src/components/fields/UrlField.svelte +0 -14
- package/src/components/ui/alert/alert-description.svelte +0 -17
- package/src/components/ui/alert/alert-title.svelte +0 -17
- package/src/components/ui/alert/alert.svelte +0 -44
- package/src/components/ui/alert/index.ts +0 -15
- package/src/components/ui/alert-dialog/alert-dialog-content.svelte +0 -29
- package/src/components/ui/alert-dialog/alert-dialog-description.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-header.svelte +0 -11
- package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +0 -16
- package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +0 -6
- package/src/components/ui/alert-dialog/alert-dialog-title.svelte +0 -16
- package/src/components/ui/alert-dialog/index.ts +0 -20
- package/src/components/ui/avatar/avatar.svelte +0 -63
- package/src/components/ui/avatar/index.ts +0 -2
- package/src/components/ui/badge/badge.svelte +0 -49
- package/src/components/ui/badge/index.ts +0 -3
- package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-item.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-link.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-list.svelte +0 -15
- package/src/components/ui/breadcrumb/breadcrumb-page.svelte +0 -18
- package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +0 -22
- package/src/components/ui/breadcrumb/breadcrumb.svelte +0 -11
- package/src/components/ui/breadcrumb/index.ts +0 -7
- package/src/components/ui/button/button.svelte +0 -90
- package/src/components/ui/button/index.ts +0 -18
- package/src/components/ui/card/card-action.svelte +0 -23
- package/src/components/ui/card/card-content.svelte +0 -20
- package/src/components/ui/card/card-description.svelte +0 -20
- package/src/components/ui/card/card-footer.svelte +0 -20
- package/src/components/ui/card/card-header.svelte +0 -23
- package/src/components/ui/card/card-title.svelte +0 -15
- package/src/components/ui/card/card.svelte +0 -22
- package/src/components/ui/card/index.ts +0 -25
- package/src/components/ui/checkbox/checkbox.svelte +0 -39
- package/src/components/ui/checkbox/index.ts +0 -6
- package/src/components/ui/collapsible/index.ts +0 -7
- package/src/components/ui/command/command-dialog.svelte +0 -31
- package/src/components/ui/command/index.ts +0 -2
- package/src/components/ui/context-menu/index.ts +0 -11
- package/src/components/ui/dialog/dialog-close.svelte +0 -11
- package/src/components/ui/dialog/dialog-content.svelte +0 -48
- package/src/components/ui/dialog/dialog-description.svelte +0 -17
- package/src/components/ui/dialog/dialog-footer.svelte +0 -32
- package/src/components/ui/dialog/dialog-header.svelte +0 -20
- package/src/components/ui/dialog/dialog-overlay.svelte +0 -17
- package/src/components/ui/dialog/dialog-portal.svelte +0 -7
- package/src/components/ui/dialog/dialog-title.svelte +0 -17
- package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
- package/src/components/ui/dialog/dialog.svelte +0 -7
- package/src/components/ui/dialog/index.ts +0 -34
- package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -43
- package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -35
- package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -36
- package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -10
- package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -27
- package/src/components/ui/dropdown-menu/dropdown-menu.svelte +0 -45
- package/src/components/ui/dropdown-menu/index.ts +0 -21
- package/src/components/ui/input/index.ts +0 -7
- package/src/components/ui/input/input.svelte +0 -48
- package/src/components/ui/label/index.ts +0 -1
- package/src/components/ui/label/label.svelte +0 -18
- package/src/components/ui/pagination/index.ts +0 -7
- package/src/components/ui/pagination/pagination-content.svelte +0 -11
- package/src/components/ui/pagination/pagination-ellipsis.svelte +0 -12
- package/src/components/ui/pagination/pagination-item.svelte +0 -11
- package/src/components/ui/pagination/pagination-link.svelte +0 -26
- package/src/components/ui/pagination/pagination-next.svelte +0 -22
- package/src/components/ui/pagination/pagination-previous.svelte +0 -22
- package/src/components/ui/pagination/pagination.svelte +0 -11
- package/src/components/ui/popover/index.ts +0 -12
- package/src/components/ui/popover/popover-content.svelte +0 -25
- package/src/components/ui/progress/index.ts +0 -1
- package/src/components/ui/progress/progress.svelte +0 -22
- package/src/components/ui/scroll-area/index.ts +0 -1
- package/src/components/ui/scroll-area/scroll-area.svelte +0 -20
- package/src/components/ui/select/index.ts +0 -2
- package/src/components/ui/select/select.svelte +0 -36
- package/src/components/ui/separator/index.ts +0 -7
- package/src/components/ui/separator/separator.svelte +0 -23
- package/src/components/ui/sheet/index.ts +0 -16
- package/src/components/ui/sheet/sheet-content.svelte +0 -28
- package/src/components/ui/sheet/sheet-description.svelte +0 -22
- package/src/components/ui/sheet/sheet-footer.svelte +0 -22
- package/src/components/ui/sheet/sheet-header.svelte +0 -22
- package/src/components/ui/sheet/sheet-title.svelte +0 -22
- package/src/components/ui/sheet/sheet.svelte +0 -78
- package/src/components/ui/skeleton/index.ts +0 -2
- package/src/components/ui/skeleton/skeleton.svelte +0 -21
- package/src/components/ui/switch/index.ts +0 -2
- package/src/components/ui/switch/switch.svelte +0 -49
- package/src/components/ui/table/index.ts +0 -28
- package/src/components/ui/table/table-body.svelte +0 -15
- package/src/components/ui/table/table-caption.svelte +0 -20
- package/src/components/ui/table/table-cell.svelte +0 -15
- package/src/components/ui/table/table-footer.svelte +0 -20
- package/src/components/ui/table/table-head.svelte +0 -15
- package/src/components/ui/table/table-header.svelte +0 -20
- package/src/components/ui/table/table-row.svelte +0 -15
- package/src/components/ui/table/table.svelte +0 -17
- package/src/components/ui/tabs/index.ts +0 -15
- package/src/components/ui/tabs/tabs-content.svelte +0 -30
- package/src/components/ui/tabs/tabs-list.svelte +0 -26
- package/src/components/ui/tabs/tabs-trigger.svelte +0 -37
- package/src/components/ui/tabs/tabs.svelte +0 -27
- package/src/components/ui/textarea/index.ts +0 -2
- package/src/components/ui/textarea/textarea.svelte +0 -24
- package/src/components/ui/tooltip/index.ts +0 -19
- package/src/components/ui/tooltip/tooltip-content.svelte +0 -52
- package/src/components/ui/tooltip/tooltip-portal.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
- package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
- package/src/components/ui/tooltip/tooltip.svelte +0 -10
- package/src/editor-config.svelte.ts +0 -23
- package/src/index.ts +0 -155
- package/src/markdown-deps.d.ts +0 -21
- package/src/router-state.svelte.ts +0 -65
- package/src/types.ts +0 -33
- package/src/utils.ts +0 -28
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { getResources, canAccessAsync } from '@svadmin/core';
|
|
3
|
-
import type { Identity, MenuItem } from '@svadmin/core';
|
|
4
|
-
import { currentPath, navigate } from '@svadmin/core/router';
|
|
5
|
-
import { t, getLocale, setLocale, getAvailableLocales } from '@svadmin/core/i18n';
|
|
6
|
-
import { toggleTheme, getResolvedTheme, colorThemes, getColorTheme, setColorTheme } from '@svadmin/core';
|
|
7
|
-
import { Button } from './ui/button/index.js';
|
|
8
|
-
import TooltipButton from './TooltipButton.svelte';
|
|
9
|
-
import * as Tooltip from './ui/tooltip/index.js';
|
|
10
|
-
import SidebarItem from './SidebarItem.svelte';
|
|
11
|
-
|
|
12
|
-
import { ScrollArea } from './ui/scroll-area/index.js';
|
|
13
|
-
import * as Collapsible from './ui/collapsible/index.js';
|
|
14
|
-
import { Avatar } from './ui/avatar/index.js';
|
|
15
|
-
import {
|
|
16
|
-
LayoutDashboard, FileText, Users, Settings, Home,
|
|
17
|
-
ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
|
|
18
|
-
Image as ImageIcon, Layout, Folder, Type, Video,
|
|
19
|
-
Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench
|
|
20
|
-
} from '@lucide/svelte';
|
|
21
|
-
|
|
22
|
-
let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
|
|
23
|
-
collapsed: boolean;
|
|
24
|
-
identity: Identity | null;
|
|
25
|
-
title: string;
|
|
26
|
-
onToggle: () => void;
|
|
27
|
-
onLogout: () => void;
|
|
28
|
-
menu?: MenuItem[];
|
|
29
|
-
/** 'hash' for SPA hash routing, 'path' for SvelteKit/filesystem routing, 'auto' to detect */
|
|
30
|
-
routeMode?: 'hash' | 'path' | 'auto';
|
|
31
|
-
} = $props();
|
|
32
|
-
|
|
33
|
-
const effectiveRouteMode = $derived(
|
|
34
|
-
routeMode === 'auto'
|
|
35
|
-
? (typeof window !== 'undefined' && window.location.hash.startsWith('#/') ? 'hash' : 'path')
|
|
36
|
-
: routeMode
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const resources = getResources();
|
|
40
|
-
|
|
41
|
-
const iconMap: Record<string, typeof LayoutDashboard> = {
|
|
42
|
-
'dashboard': LayoutDashboard,
|
|
43
|
-
'posts': FileText,
|
|
44
|
-
'users': Users,
|
|
45
|
-
'settings': Settings,
|
|
46
|
-
'home': Home,
|
|
47
|
-
'palette': Palette,
|
|
48
|
-
'image': ImageIcon,
|
|
49
|
-
'layout': Layout,
|
|
50
|
-
'folder': Folder,
|
|
51
|
-
'type': Type,
|
|
52
|
-
'video': Video,
|
|
53
|
-
'download': Download,
|
|
54
|
-
'list-todo': ListTodo,
|
|
55
|
-
'trending-up': TrendingUp,
|
|
56
|
-
'sparkles': Sparkles,
|
|
57
|
-
'images': Images,
|
|
58
|
-
'bot': Bot,
|
|
59
|
-
'key': Key,
|
|
60
|
-
'key-round': KeyRound,
|
|
61
|
-
'credit-card': CreditCard,
|
|
62
|
-
'book-open': BookOpen,
|
|
63
|
-
'wrench': Wrench,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
interface NavItem {
|
|
67
|
-
path: string;
|
|
68
|
-
label: string;
|
|
69
|
-
Icon: typeof LayoutDashboard;
|
|
70
|
-
group?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface NavGroup {
|
|
74
|
-
name: string | null; // null = ungrouped
|
|
75
|
-
items: NavItem[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Use effect to build nav items, taking access control into account
|
|
79
|
-
let navItems = $state.raw<NavItem[]>([]);
|
|
80
|
-
|
|
81
|
-
$effect(() => {
|
|
82
|
-
// Re-run when locale or resources change
|
|
83
|
-
const localeVal = getLocale();
|
|
84
|
-
|
|
85
|
-
Promise.all(resources.map(async (r) => {
|
|
86
|
-
try {
|
|
87
|
-
const { can } = await canAccessAsync(r.name, 'list');
|
|
88
|
-
return { r, can };
|
|
89
|
-
} catch {
|
|
90
|
-
return { r, can: false };
|
|
91
|
-
}
|
|
92
|
-
})).then(results => {
|
|
93
|
-
const items: NavItem[] = [{ path: '/', label: t('common.home'), Icon: LayoutDashboard }];
|
|
94
|
-
for (const { r, can } of results) {
|
|
95
|
-
if (can) {
|
|
96
|
-
items.push({
|
|
97
|
-
path: `/${r.name}`,
|
|
98
|
-
label: r.label,
|
|
99
|
-
Icon: iconMap[r.icon ?? r.name] ?? Settings,
|
|
100
|
-
group: r.group,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
navItems = items;
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
/** Toggle between available locales */
|
|
109
|
-
function toggleLocale() {
|
|
110
|
-
const locales = getAvailableLocales();
|
|
111
|
-
const current = getLocale();
|
|
112
|
-
const idx = locales.indexOf(current);
|
|
113
|
-
const next = locales[(idx + 1) % locales.length];
|
|
114
|
-
setLocale(next);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Short display label for current locale */
|
|
118
|
-
const localeLabel = $derived(getLocale() === 'zh-CN' ? '中' : 'EN');
|
|
119
|
-
|
|
120
|
-
// Track current hash for active state
|
|
121
|
-
let path = $state(currentPath());
|
|
122
|
-
|
|
123
|
-
function onHashChange() { path = currentPath(); }
|
|
124
|
-
|
|
125
|
-
function isActive(itemPath: string): boolean {
|
|
126
|
-
if (itemPath === '/') return path === '/';
|
|
127
|
-
return path === itemPath || path.startsWith(itemPath + "/");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Prefetch resource data on hover for instant navigation */
|
|
131
|
-
const prefetchedResources = new Set<string>();
|
|
132
|
-
function prefetchResource(resourceName: string) {
|
|
133
|
-
if (resourceName === '/' || prefetchedResources.has(resourceName)) return;
|
|
134
|
-
prefetchedResources.add(resourceName);
|
|
135
|
-
// Trigger a background fetch by navigating the browser cache
|
|
136
|
-
// The actual prefetch happens via the #key block re-creating useList in AdminApp
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Group nav items by group field (null = ungrouped)
|
|
140
|
-
const navGroups = $derived.by((): NavGroup[] => {
|
|
141
|
-
const groups: NavGroup[] = [];
|
|
142
|
-
const groupMap = new Map<string | null, NavItem[]>();
|
|
143
|
-
|
|
144
|
-
for (const item of navItems) {
|
|
145
|
-
const key = item.group ?? null;
|
|
146
|
-
if (!groupMap.has(key)) {
|
|
147
|
-
groupMap.set(key, []);
|
|
148
|
-
groups.push({ name: key, items: groupMap.get(key)! });
|
|
149
|
-
}
|
|
150
|
-
groupMap.get(key)!.push(item);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return groups;
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Track which groups are open
|
|
157
|
-
let openGroups = $state<Set<string>>(new Set());
|
|
158
|
-
let colorPickerOpen = $state(false);
|
|
159
|
-
let colorPickerRef = $state<HTMLDivElement | null>(null);
|
|
160
|
-
let colorPickerOpenedAt = 0;
|
|
161
|
-
|
|
162
|
-
// Click-outside to close color picker
|
|
163
|
-
$effect(() => {
|
|
164
|
-
if (!colorPickerOpen) return;
|
|
165
|
-
function handleMouseDown(e: MouseEvent) {
|
|
166
|
-
// Ignore if opened within last 200ms (same interaction)
|
|
167
|
-
if (Date.now() - colorPickerOpenedAt < 200) return;
|
|
168
|
-
if (colorPickerRef && !colorPickerRef.contains(e.target as Node)) {
|
|
169
|
-
colorPickerOpen = false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
document.addEventListener('mousedown', handleMouseDown, true);
|
|
173
|
-
return () => {
|
|
174
|
-
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
</script>
|
|
178
|
-
|
|
179
|
-
<svelte:window onhashchange={onHashChange} />
|
|
180
|
-
|
|
181
|
-
<aside
|
|
182
|
-
aria-label="Sidebar navigation"
|
|
183
|
-
class="fixed inset-y-0 left-0 z-30 flex flex-col backdrop-blur-xl transition-all duration-300"
|
|
184
|
-
style="background-color: var(--sidebar);"
|
|
185
|
-
class:w-64={!collapsed}
|
|
186
|
-
class:w-16={collapsed}
|
|
187
|
-
>
|
|
188
|
-
<!-- Logo -->
|
|
189
|
-
<div class="flex h-16 items-center justify-between px-4">
|
|
190
|
-
{#if !collapsed}
|
|
191
|
-
<div class="flex items-center gap-3">
|
|
192
|
-
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
|
|
193
|
-
<span class="font-semibold text-lg tracking-tight text-sidebar-foreground">{title}</span>
|
|
194
|
-
</div>
|
|
195
|
-
{:else}
|
|
196
|
-
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-primary to-purple-500 shrink-0 mx-auto" style="box-shadow: 0 4px 12px oklch(0.488 0.243 264.376 / 20%);"></div>
|
|
197
|
-
{/if}
|
|
198
|
-
{#if !collapsed}
|
|
199
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
|
|
200
|
-
<ChevronLeft class="h-4 w-4" />
|
|
201
|
-
</TooltipButton>
|
|
202
|
-
{:else}
|
|
203
|
-
<TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mt-2 mb-2 block">
|
|
204
|
-
<ChevronRight class="h-4 w-4" />
|
|
205
|
-
</TooltipButton>
|
|
206
|
-
{/if}
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<ScrollArea class="flex-1">
|
|
210
|
-
<nav aria-label="Main menu" class="py-4 px-3 space-y-1">
|
|
211
|
-
{#if menu && menu.length > 0}
|
|
212
|
-
<!-- Custom multi-level menu -->
|
|
213
|
-
{#each menu as item}
|
|
214
|
-
<SidebarItem {item} currentPath={path} {collapsed} depth={0} />
|
|
215
|
-
{/each}
|
|
216
|
-
{:else}
|
|
217
|
-
<!-- Auto-generated menu from resources (fallback) -->
|
|
218
|
-
{#each navGroups as group}
|
|
219
|
-
{#if group.name && !collapsed}
|
|
220
|
-
<!-- Grouped section with Collapsible -->
|
|
221
|
-
<Collapsible.Root open={openGroups.has(group.name)} onOpenChange={(isOpen) => {
|
|
222
|
-
const next = new Set(openGroups);
|
|
223
|
-
if (isOpen) next.add(group.name!); else next.delete(group.name!);
|
|
224
|
-
openGroups = next;
|
|
225
|
-
}}>
|
|
226
|
-
<Collapsible.Trigger
|
|
227
|
-
class="flex w-full items-center justify-between px-3 py-1.5 text-sm font-semibold tracking-wider text-sidebar-foreground/60 hover:text-sidebar-foreground transition-colors"
|
|
228
|
-
>
|
|
229
|
-
<span>{group.name}</span>
|
|
230
|
-
<ChevronDown class="h-3 w-3 transition-transform {openGroups.has(group.name) ? 'rotate-180' : ''}" />
|
|
231
|
-
</Collapsible.Trigger>
|
|
232
|
-
<Collapsible.Content>
|
|
233
|
-
<div class="mt-1 space-y-1">
|
|
234
|
-
{#each group.items as item}
|
|
235
|
-
{@const active = isActive(item.path)}
|
|
236
|
-
<a
|
|
237
|
-
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
238
|
-
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
|
|
239
|
-
{active
|
|
240
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
241
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
242
|
-
>
|
|
243
|
-
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
244
|
-
<span>{item.label}</span>
|
|
245
|
-
</a>
|
|
246
|
-
{/each}
|
|
247
|
-
</div>
|
|
248
|
-
</Collapsible.Content>
|
|
249
|
-
</Collapsible.Root>
|
|
250
|
-
{:else}
|
|
251
|
-
<!-- Ungrouped items (flat) -->
|
|
252
|
-
{#each group.items as item}
|
|
253
|
-
{@const active = isActive(item.path)}
|
|
254
|
-
{#if collapsed}
|
|
255
|
-
<Tooltip.Root>
|
|
256
|
-
<Tooltip.Trigger>
|
|
257
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
258
|
-
<a
|
|
259
|
-
{...props}
|
|
260
|
-
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
261
|
-
class="flex items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
262
|
-
{active
|
|
263
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
264
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
265
|
-
>
|
|
266
|
-
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
267
|
-
</a>
|
|
268
|
-
{/snippet}
|
|
269
|
-
</Tooltip.Trigger>
|
|
270
|
-
<Tooltip.Content side="right">
|
|
271
|
-
{item.label}
|
|
272
|
-
</Tooltip.Content>
|
|
273
|
-
</Tooltip.Root>
|
|
274
|
-
{:else}
|
|
275
|
-
<a
|
|
276
|
-
href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
|
|
277
|
-
class="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
278
|
-
{active
|
|
279
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
280
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
281
|
-
>
|
|
282
|
-
<item.Icon class="h-4 w-4 flex-shrink-0" />
|
|
283
|
-
<span>{item.label}</span>
|
|
284
|
-
</a>
|
|
285
|
-
{/if}
|
|
286
|
-
{/each}
|
|
287
|
-
{/if}
|
|
288
|
-
{/each}
|
|
289
|
-
{/if}
|
|
290
|
-
</nav>
|
|
291
|
-
</ScrollArea>
|
|
292
|
-
|
|
293
|
-
<!-- Footer -->
|
|
294
|
-
<div>
|
|
295
|
-
<!-- Color theme picker -->
|
|
296
|
-
{#if !collapsed}
|
|
297
|
-
<div class="px-3 pt-3 pb-1">
|
|
298
|
-
<div class="relative" bind:this={colorPickerRef}>
|
|
299
|
-
<Button
|
|
300
|
-
variant="ghost"
|
|
301
|
-
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"
|
|
302
|
-
onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
|
|
303
|
-
>
|
|
304
|
-
<Palette class="h-4 w-4" />
|
|
305
|
-
<span class="flex-1 text-left text-xs">{t('common.toggleTheme')}</span>
|
|
306
|
-
<span
|
|
307
|
-
class="h-3.5 w-3.5 rounded-full ring-1 ring-offset-1 ring-offset-sidebar"
|
|
308
|
-
style="background-color: {colorThemes.find(c => c.id === getColorTheme())?.color ?? '#6366f1'}; --tw-ring-color: {colorThemes.find(c => c.id === getColorTheme())?.color ?? '#6366f1'}"
|
|
309
|
-
></span>
|
|
310
|
-
</Button>
|
|
311
|
-
{#if colorPickerOpen}
|
|
312
|
-
<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">
|
|
313
|
-
{#each colorThemes as ct}
|
|
314
|
-
<button
|
|
315
|
-
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"
|
|
316
|
-
onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
|
|
317
|
-
>
|
|
318
|
-
<span
|
|
319
|
-
class="h-3.5 w-3.5 rounded-full {getColorTheme() === ct.id ? 'ring-2 ring-offset-1 scale-110' : 'opacity-70'}"
|
|
320
|
-
style="background-color: {ct.color}; {getColorTheme() === ct.id ? `--tw-ring-color: ${ct.color}` : ''}"
|
|
321
|
-
></span>
|
|
322
|
-
<span class="text-xs">{ct.label}</span>
|
|
323
|
-
</button>
|
|
324
|
-
{/each}
|
|
325
|
-
</div>
|
|
326
|
-
{/if}
|
|
327
|
-
</div>
|
|
328
|
-
</div>
|
|
329
|
-
{/if}
|
|
330
|
-
|
|
331
|
-
<!-- User info -->
|
|
332
|
-
{#if !collapsed && identity}
|
|
333
|
-
<div class="px-3 pb-3 pt-1">
|
|
334
|
-
<div class="flex items-center gap-3 rounded-lg p-2 hover:bg-sidebar-accent/50 transition-colors">
|
|
335
|
-
<Avatar
|
|
336
|
-
src={(identity as Record<string, unknown>).avatar as string | undefined}
|
|
337
|
-
alt={identity.name ?? 'User'}
|
|
338
|
-
fallback={identity.name?.charAt(0).toUpperCase() ?? 'U'}
|
|
339
|
-
size="sm"
|
|
340
|
-
/>
|
|
341
|
-
<div class="flex-1 min-w-0">
|
|
342
|
-
<p class="truncate text-sm font-medium text-sidebar-foreground">{identity.name}</p>
|
|
343
|
-
<p class="truncate text-[10px] text-sidebar-foreground/50">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
|
|
344
|
-
</div>
|
|
345
|
-
</div>
|
|
346
|
-
<div class="flex items-center justify-between px-1 mt-1">
|
|
347
|
-
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
348
|
-
<span class="text-xs font-bold">{localeLabel}</span>
|
|
349
|
-
</TooltipButton>
|
|
350
|
-
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
351
|
-
{#if getResolvedTheme() === 'dark'}
|
|
352
|
-
<Sun class="h-4 w-4" />
|
|
353
|
-
{:else}
|
|
354
|
-
<Moon class="h-4 w-4" />
|
|
355
|
-
{/if}
|
|
356
|
-
</TooltipButton>
|
|
357
|
-
<TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
358
|
-
<Settings class="h-4 w-4" />
|
|
359
|
-
</TooltipButton>
|
|
360
|
-
<TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground/60 hover:text-destructive">
|
|
361
|
-
<LogOut class="h-4 w-4" />
|
|
362
|
-
</TooltipButton>
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
{:else if collapsed}
|
|
366
|
-
<div class="px-1 py-3 space-y-1">
|
|
367
|
-
<Tooltip.Root>
|
|
368
|
-
<Tooltip.Trigger>
|
|
369
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
370
|
-
<Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
371
|
-
<span class="text-xs font-bold">{localeLabel}</span>
|
|
372
|
-
</Button>
|
|
373
|
-
{/snippet}
|
|
374
|
-
</Tooltip.Trigger>
|
|
375
|
-
<Tooltip.Content side="right">{t('common.switchLanguage')}</Tooltip.Content>
|
|
376
|
-
</Tooltip.Root>
|
|
377
|
-
<Tooltip.Root>
|
|
378
|
-
<Tooltip.Trigger>
|
|
379
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
380
|
-
<Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
381
|
-
{#if getResolvedTheme() === 'dark'}
|
|
382
|
-
<Sun class="h-4 w-4" />
|
|
383
|
-
{:else}
|
|
384
|
-
<Moon class="h-4 w-4" />
|
|
385
|
-
{/if}
|
|
386
|
-
</Button>
|
|
387
|
-
{/snippet}
|
|
388
|
-
</Tooltip.Trigger>
|
|
389
|
-
<Tooltip.Content side="right">{t('common.toggleTheme')}</Tooltip.Content>
|
|
390
|
-
</Tooltip.Root>
|
|
391
|
-
<Tooltip.Root>
|
|
392
|
-
<Tooltip.Trigger>
|
|
393
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
394
|
-
<Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground/60 hover:text-destructive">
|
|
395
|
-
<LogOut class="h-4 w-4" />
|
|
396
|
-
</Button>
|
|
397
|
-
{/snippet}
|
|
398
|
-
</Tooltip.Trigger>
|
|
399
|
-
<Tooltip.Content side="right">{t('common.logout')}</Tooltip.Content>
|
|
400
|
-
</Tooltip.Root>
|
|
401
|
-
</div>
|
|
402
|
-
{:else}
|
|
403
|
-
<div class="flex gap-1 p-3">
|
|
404
|
-
<TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
405
|
-
<span class="text-xs font-bold">{localeLabel}</span>
|
|
406
|
-
</TooltipButton>
|
|
407
|
-
<TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground/60 hover:text-sidebar-foreground">
|
|
408
|
-
{#if getResolvedTheme() === 'dark'}
|
|
409
|
-
<Sun class="h-4 w-4" />
|
|
410
|
-
{:else}
|
|
411
|
-
<Moon class="h-4 w-4" />
|
|
412
|
-
{/if}
|
|
413
|
-
</TooltipButton>
|
|
414
|
-
</div>
|
|
415
|
-
{/if}
|
|
416
|
-
</div>
|
|
417
|
-
</aside>
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { MenuItem } from '@svadmin/core';
|
|
3
|
-
import { t } from '@svadmin/core/i18n';
|
|
4
|
-
import * as Collapsible from './ui/collapsible/index.js';
|
|
5
|
-
import * as Tooltip from './ui/tooltip/index.js';
|
|
6
|
-
import SidebarItem from './SidebarItem.svelte';
|
|
7
|
-
import {
|
|
8
|
-
LayoutDashboard, FileText, Users, Settings, Home,
|
|
9
|
-
ChevronDown, Folder, ExternalLink, type Icon as LucideIcon,
|
|
10
|
-
} from '@lucide/svelte';
|
|
11
|
-
|
|
12
|
-
let { item, currentPath, collapsed = false, depth = 0 }: {
|
|
13
|
-
item: MenuItem;
|
|
14
|
-
currentPath: string;
|
|
15
|
-
collapsed?: boolean;
|
|
16
|
-
depth?: number;
|
|
17
|
-
} = $props();
|
|
18
|
-
|
|
19
|
-
const iconMap: Record<string, typeof LayoutDashboard> = {
|
|
20
|
-
dashboard: LayoutDashboard,
|
|
21
|
-
posts: FileText,
|
|
22
|
-
users: Users,
|
|
23
|
-
settings: Settings,
|
|
24
|
-
home: Home,
|
|
25
|
-
folder: Folder,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function getIcon(name?: string): typeof LayoutDashboard {
|
|
29
|
-
if (!name) return depth === 0 ? Settings : Folder;
|
|
30
|
-
return iconMap[name] ?? Settings;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getLabel(mi: MenuItem): string {
|
|
34
|
-
return mi.label ?? mi.name;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isActive(itemHref: string | undefined): boolean {
|
|
38
|
-
if (!itemHref) return false;
|
|
39
|
-
if (itemHref === '/') return currentPath === '/';
|
|
40
|
-
return currentPath.startsWith(itemHref);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Check if any child (recursively) is active — used to auto-open parent groups */
|
|
44
|
-
function hasActiveChild(menuItem: MenuItem): boolean {
|
|
45
|
-
if (isActive(menuItem.href)) return true;
|
|
46
|
-
return menuItem.children?.some((c: MenuItem) => hasActiveChild(c)) ?? false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const hasChildren = $derived(item.children && item.children.length > 0);
|
|
50
|
-
const active = $derived(isActive(item.href));
|
|
51
|
-
const childActive = $derived(hasActiveChild(item));
|
|
52
|
-
const isExternal = $derived(item.target === '_blank');
|
|
53
|
-
|
|
54
|
-
// Track open state — auto-open if a child is active
|
|
55
|
-
let isOpen = $state(false);
|
|
56
|
-
$effect(() => { if (childActive) isOpen = true; });
|
|
57
|
-
|
|
58
|
-
const Icon = $derived(getIcon(item.icon));
|
|
59
|
-
const label = $derived(getLabel(item));
|
|
60
|
-
</script>
|
|
61
|
-
|
|
62
|
-
{#if item.meta?.hidden}
|
|
63
|
-
<!-- hidden item, render nothing -->
|
|
64
|
-
{:else if hasChildren && !collapsed}
|
|
65
|
-
<!-- Parent node with children: render as collapsible group -->
|
|
66
|
-
<Collapsible.Root bind:open={isOpen}>
|
|
67
|
-
<Collapsible.Trigger
|
|
68
|
-
class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
|
|
69
|
-
{childActive
|
|
70
|
-
? 'text-sidebar-foreground bg-sidebar-accent/30'
|
|
71
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
72
|
-
style="padding-left: {12 + depth * 12}px"
|
|
73
|
-
>
|
|
74
|
-
<span class="flex items-center gap-3">
|
|
75
|
-
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
76
|
-
<span>{label}</span>
|
|
77
|
-
</span>
|
|
78
|
-
<ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
|
|
79
|
-
</Collapsible.Trigger>
|
|
80
|
-
<Collapsible.Content>
|
|
81
|
-
<div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
|
|
82
|
-
{#each item.children as child}
|
|
83
|
-
<SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
|
|
84
|
-
{/each}
|
|
85
|
-
</div>
|
|
86
|
-
</Collapsible.Content>
|
|
87
|
-
</Collapsible.Root>
|
|
88
|
-
{:else if collapsed}
|
|
89
|
-
<!-- Collapsed sidebar: icon-only with tooltip -->
|
|
90
|
-
<Tooltip.Root>
|
|
91
|
-
<Tooltip.Trigger>
|
|
92
|
-
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
93
|
-
<a
|
|
94
|
-
{...props}
|
|
95
|
-
href={item.href ? `#${item.href}` : undefined}
|
|
96
|
-
target={isExternal ? '_blank' : undefined}
|
|
97
|
-
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
98
|
-
class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
|
|
99
|
-
{active
|
|
100
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
|
101
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
102
|
-
>
|
|
103
|
-
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
104
|
-
</a>
|
|
105
|
-
{/snippet}
|
|
106
|
-
</Tooltip.Trigger>
|
|
107
|
-
<Tooltip.Content side="right">
|
|
108
|
-
{label}
|
|
109
|
-
</Tooltip.Content>
|
|
110
|
-
</Tooltip.Root>
|
|
111
|
-
{:else}
|
|
112
|
-
<!-- Leaf node: render as a link -->
|
|
113
|
-
<a
|
|
114
|
-
href={item.href ? `#${item.href}` : undefined}
|
|
115
|
-
target={isExternal ? '_blank' : undefined}
|
|
116
|
-
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
117
|
-
class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
|
|
118
|
-
{active
|
|
119
|
-
? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active'
|
|
120
|
-
: 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
|
|
121
|
-
style="padding-left: {12 + depth * 12}px"
|
|
122
|
-
>
|
|
123
|
-
<Icon class="h-4 w-4 flex-shrink-0" />
|
|
124
|
-
<span class="flex-1">{label}</span>
|
|
125
|
-
{#if isExternal}
|
|
126
|
-
<ExternalLink class="h-3 w-3 opacity-50" />
|
|
127
|
-
{/if}
|
|
128
|
-
</a>
|
|
129
|
-
{/if}
|