@svadmin/ui 0.29.0 → 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.
Files changed (224) hide show
  1. package/package.json +20 -9
  2. package/src/README.md +0 -18
  3. package/src/actions.ts +0 -110
  4. package/src/app.css +0 -519
  5. package/src/component-registry.svelte.ts +0 -60
  6. package/src/components/AICommandBar.svelte +0 -180
  7. package/src/components/AboutSettings.svelte +0 -106
  8. package/src/components/AdminApp.svelte +0 -199
  9. package/src/components/AnimatedCounter.svelte +0 -46
  10. package/src/components/AnomalyBadge.svelte +0 -65
  11. package/src/components/AppearanceSettings.svelte +0 -183
  12. package/src/components/AuditLogDrawer.svelte +0 -93
  13. package/src/components/AuditLogViewer.svelte +0 -226
  14. package/src/components/Authenticated.svelte +0 -51
  15. package/src/components/AutoForm.svelte +0 -242
  16. package/src/components/AutoSaveIndicator.svelte +0 -35
  17. package/src/components/AutoTable.svelte +0 -839
  18. package/src/components/Breadcrumbs.svelte +0 -58
  19. package/src/components/CanAccess.svelte +0 -23
  20. package/src/components/CatchAllNavigate.svelte +0 -21
  21. package/src/components/ChatDialog.svelte +0 -535
  22. package/src/components/ComboboxField.svelte +0 -129
  23. package/src/components/CommandPalette.svelte +0 -135
  24. package/src/components/ConfigErrorScreen.svelte +0 -126
  25. package/src/components/ConfirmDialog.svelte +0 -53
  26. package/src/components/CopilotPanel.svelte +0 -163
  27. package/src/components/CreatePage.svelte +0 -39
  28. package/src/components/DevTools.svelte +0 -149
  29. package/src/components/DraggableGrid.svelte +0 -86
  30. package/src/components/DraggableHeader.svelte +0 -112
  31. package/src/components/DrawerForm.svelte +0 -35
  32. package/src/components/EditPage.svelte +0 -52
  33. package/src/components/EmptyState.svelte +0 -31
  34. package/src/components/ErrorBoundary.svelte +0 -49
  35. package/src/components/ErrorComponent.svelte +0 -36
  36. package/src/components/FieldRenderer.svelte +0 -357
  37. package/src/components/ForgotPasswordPage.svelte +0 -112
  38. package/src/components/Header.svelte +0 -94
  39. package/src/components/InferencerPanel.svelte +0 -192
  40. package/src/components/InfiniteList.svelte +0 -93
  41. package/src/components/InlineEdit.svelte +0 -88
  42. package/src/components/InsightCard.svelte +0 -98
  43. package/src/components/KeyboardShortcuts.svelte +0 -61
  44. package/src/components/Layout.svelte +0 -167
  45. package/src/components/ListPage.svelte +0 -71
  46. package/src/components/LiveIndicator.svelte +0 -58
  47. package/src/components/LoginPage.svelte +0 -163
  48. package/src/components/MarkdownRenderer.svelte +0 -127
  49. package/src/components/ModalForm.svelte +0 -38
  50. package/src/components/NavigateToResource.svelte +0 -20
  51. package/src/components/PageHeader.svelte +0 -39
  52. package/src/components/PageSkeleton.svelte +0 -71
  53. package/src/components/PasswordInput.svelte +0 -96
  54. package/src/components/PermissionMatrix.svelte +0 -185
  55. package/src/components/ProfilePage.svelte +0 -284
  56. package/src/components/RegisterPage.svelte +0 -112
  57. package/src/components/RolesSettings.svelte +0 -146
  58. package/src/components/SettingsPage.svelte +0 -113
  59. package/src/components/ShowPage.svelte +0 -73
  60. package/src/components/Sidebar.svelte +0 -417
  61. package/src/components/SidebarItem.svelte +0 -129
  62. package/src/components/SmartSuggest.svelte +0 -158
  63. package/src/components/StatsCard.svelte +0 -79
  64. package/src/components/StepsForm.svelte +0 -192
  65. package/src/components/TaskQueueDrawer.svelte +0 -158
  66. package/src/components/TenantSwitcher.svelte +0 -84
  67. package/src/components/ThemedTitle.svelte +0 -32
  68. package/src/components/Toast.svelte +0 -46
  69. package/src/components/TooltipButton.svelte +0 -42
  70. package/src/components/UndoableNotification.svelte +0 -69
  71. package/src/components/UpdatePasswordPage.svelte +0 -89
  72. package/src/components/VirtualTable.svelte +0 -76
  73. package/src/components/VoiceInput.svelte +0 -120
  74. package/src/components/buttons/CloneButton.svelte +0 -30
  75. package/src/components/buttons/CreateButton.svelte +0 -29
  76. package/src/components/buttons/DeleteButton.svelte +0 -59
  77. package/src/components/buttons/EditButton.svelte +0 -30
  78. package/src/components/buttons/ExportButton.svelte +0 -30
  79. package/src/components/buttons/ImportButton.svelte +0 -56
  80. package/src/components/buttons/ListButton.svelte +0 -23
  81. package/src/components/buttons/RefreshButton.svelte +0 -36
  82. package/src/components/buttons/SaveButton.svelte +0 -27
  83. package/src/components/buttons/ShowButton.svelte +0 -31
  84. package/src/components/buttons/index.ts +0 -10
  85. package/src/components/charts/BarChart.svelte +0 -69
  86. package/src/components/charts/LineChart.svelte +0 -115
  87. package/src/components/charts/PieChart.svelte +0 -95
  88. package/src/components/charts/index.ts +0 -3
  89. package/src/components/fieldComponentMap.ts +0 -79
  90. package/src/components/fields/ArrayField.svelte +0 -86
  91. package/src/components/fields/BooleanField.svelte +0 -13
  92. package/src/components/fields/DateField.svelte +0 -6
  93. package/src/components/fields/EmailField.svelte +0 -9
  94. package/src/components/fields/FileField.svelte +0 -14
  95. package/src/components/fields/ImageField.svelte +0 -9
  96. package/src/components/fields/JsonField.svelte +0 -72
  97. package/src/components/fields/MarkdownField.svelte +0 -9
  98. package/src/components/fields/MultiSelectField.svelte +0 -21
  99. package/src/components/fields/NumberField.svelte +0 -6
  100. package/src/components/fields/RelationField.svelte +0 -24
  101. package/src/components/fields/RichTextField.svelte +0 -35
  102. package/src/components/fields/SelectField.svelte +0 -14
  103. package/src/components/fields/TagField.svelte +0 -12
  104. package/src/components/fields/TextField.svelte +0 -11
  105. package/src/components/fields/UrlField.svelte +0 -14
  106. package/src/components/ui/alert/alert-description.svelte +0 -17
  107. package/src/components/ui/alert/alert-title.svelte +0 -17
  108. package/src/components/ui/alert/alert.svelte +0 -44
  109. package/src/components/ui/alert/index.ts +0 -15
  110. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +0 -29
  111. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +0 -16
  112. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +0 -11
  113. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +0 -11
  114. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +0 -16
  115. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +0 -6
  116. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +0 -16
  117. package/src/components/ui/alert-dialog/index.ts +0 -20
  118. package/src/components/ui/avatar/avatar.svelte +0 -63
  119. package/src/components/ui/avatar/index.ts +0 -2
  120. package/src/components/ui/badge/badge.svelte +0 -49
  121. package/src/components/ui/badge/index.ts +0 -3
  122. package/src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte +0 -18
  123. package/src/components/ui/breadcrumb/breadcrumb-item.svelte +0 -15
  124. package/src/components/ui/breadcrumb/breadcrumb-link.svelte +0 -15
  125. package/src/components/ui/breadcrumb/breadcrumb-list.svelte +0 -15
  126. package/src/components/ui/breadcrumb/breadcrumb-page.svelte +0 -18
  127. package/src/components/ui/breadcrumb/breadcrumb-separator.svelte +0 -22
  128. package/src/components/ui/breadcrumb/breadcrumb.svelte +0 -11
  129. package/src/components/ui/breadcrumb/index.ts +0 -7
  130. package/src/components/ui/button/button.svelte +0 -90
  131. package/src/components/ui/button/index.ts +0 -18
  132. package/src/components/ui/card/card-action.svelte +0 -23
  133. package/src/components/ui/card/card-content.svelte +0 -20
  134. package/src/components/ui/card/card-description.svelte +0 -20
  135. package/src/components/ui/card/card-footer.svelte +0 -20
  136. package/src/components/ui/card/card-header.svelte +0 -23
  137. package/src/components/ui/card/card-title.svelte +0 -15
  138. package/src/components/ui/card/card.svelte +0 -22
  139. package/src/components/ui/card/index.ts +0 -25
  140. package/src/components/ui/checkbox/checkbox.svelte +0 -39
  141. package/src/components/ui/checkbox/index.ts +0 -6
  142. package/src/components/ui/collapsible/index.ts +0 -7
  143. package/src/components/ui/command/command-dialog.svelte +0 -31
  144. package/src/components/ui/command/index.ts +0 -2
  145. package/src/components/ui/context-menu/index.ts +0 -11
  146. package/src/components/ui/dialog/dialog-close.svelte +0 -11
  147. package/src/components/ui/dialog/dialog-content.svelte +0 -48
  148. package/src/components/ui/dialog/dialog-description.svelte +0 -17
  149. package/src/components/ui/dialog/dialog-footer.svelte +0 -32
  150. package/src/components/ui/dialog/dialog-header.svelte +0 -20
  151. package/src/components/ui/dialog/dialog-overlay.svelte +0 -17
  152. package/src/components/ui/dialog/dialog-portal.svelte +0 -7
  153. package/src/components/ui/dialog/dialog-title.svelte +0 -17
  154. package/src/components/ui/dialog/dialog-trigger.svelte +0 -11
  155. package/src/components/ui/dialog/dialog.svelte +0 -7
  156. package/src/components/ui/dialog/index.ts +0 -34
  157. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +0 -43
  158. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +0 -35
  159. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +0 -36
  160. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +0 -10
  161. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +0 -27
  162. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +0 -45
  163. package/src/components/ui/dropdown-menu/index.ts +0 -21
  164. package/src/components/ui/input/index.ts +0 -7
  165. package/src/components/ui/input/input.svelte +0 -48
  166. package/src/components/ui/label/index.ts +0 -1
  167. package/src/components/ui/label/label.svelte +0 -18
  168. package/src/components/ui/pagination/index.ts +0 -7
  169. package/src/components/ui/pagination/pagination-content.svelte +0 -11
  170. package/src/components/ui/pagination/pagination-ellipsis.svelte +0 -12
  171. package/src/components/ui/pagination/pagination-item.svelte +0 -11
  172. package/src/components/ui/pagination/pagination-link.svelte +0 -26
  173. package/src/components/ui/pagination/pagination-next.svelte +0 -22
  174. package/src/components/ui/pagination/pagination-previous.svelte +0 -22
  175. package/src/components/ui/pagination/pagination.svelte +0 -11
  176. package/src/components/ui/popover/index.ts +0 -12
  177. package/src/components/ui/popover/popover-content.svelte +0 -25
  178. package/src/components/ui/progress/index.ts +0 -1
  179. package/src/components/ui/progress/progress.svelte +0 -22
  180. package/src/components/ui/scroll-area/index.ts +0 -1
  181. package/src/components/ui/scroll-area/scroll-area.svelte +0 -20
  182. package/src/components/ui/select/index.ts +0 -2
  183. package/src/components/ui/select/select.svelte +0 -36
  184. package/src/components/ui/separator/index.ts +0 -7
  185. package/src/components/ui/separator/separator.svelte +0 -23
  186. package/src/components/ui/sheet/index.ts +0 -16
  187. package/src/components/ui/sheet/sheet-content.svelte +0 -28
  188. package/src/components/ui/sheet/sheet-description.svelte +0 -22
  189. package/src/components/ui/sheet/sheet-footer.svelte +0 -22
  190. package/src/components/ui/sheet/sheet-header.svelte +0 -22
  191. package/src/components/ui/sheet/sheet-title.svelte +0 -22
  192. package/src/components/ui/sheet/sheet.svelte +0 -78
  193. package/src/components/ui/skeleton/index.ts +0 -2
  194. package/src/components/ui/skeleton/skeleton.svelte +0 -21
  195. package/src/components/ui/switch/index.ts +0 -2
  196. package/src/components/ui/switch/switch.svelte +0 -49
  197. package/src/components/ui/table/index.ts +0 -28
  198. package/src/components/ui/table/table-body.svelte +0 -15
  199. package/src/components/ui/table/table-caption.svelte +0 -20
  200. package/src/components/ui/table/table-cell.svelte +0 -15
  201. package/src/components/ui/table/table-footer.svelte +0 -20
  202. package/src/components/ui/table/table-head.svelte +0 -15
  203. package/src/components/ui/table/table-header.svelte +0 -20
  204. package/src/components/ui/table/table-row.svelte +0 -15
  205. package/src/components/ui/table/table.svelte +0 -17
  206. package/src/components/ui/tabs/index.ts +0 -15
  207. package/src/components/ui/tabs/tabs-content.svelte +0 -30
  208. package/src/components/ui/tabs/tabs-list.svelte +0 -26
  209. package/src/components/ui/tabs/tabs-trigger.svelte +0 -37
  210. package/src/components/ui/tabs/tabs.svelte +0 -27
  211. package/src/components/ui/textarea/index.ts +0 -2
  212. package/src/components/ui/textarea/textarea.svelte +0 -24
  213. package/src/components/ui/tooltip/index.ts +0 -19
  214. package/src/components/ui/tooltip/tooltip-content.svelte +0 -52
  215. package/src/components/ui/tooltip/tooltip-portal.svelte +0 -7
  216. package/src/components/ui/tooltip/tooltip-provider.svelte +0 -7
  217. package/src/components/ui/tooltip/tooltip-trigger.svelte +0 -7
  218. package/src/components/ui/tooltip/tooltip.svelte +0 -10
  219. package/src/editor-config.svelte.ts +0 -23
  220. package/src/index.ts +0 -155
  221. package/src/markdown-deps.d.ts +0 -21
  222. package/src/router-state.svelte.ts +0 -65
  223. package/src/types.ts +0 -33
  224. 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}