@svadmin/ui 0.34.9 → 0.35.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.
Files changed (123) hide show
  1. package/dist/app.css +372 -0
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/component-registry.svelte.js +1 -0
  4. package/dist/components/AICommandBar.svelte +3 -3
  5. package/dist/components/AboutSettings.svelte +2 -2
  6. package/dist/components/AdminApp.svelte +96 -18
  7. package/dist/components/AdminApp.svelte.d.ts +32 -2
  8. package/dist/components/AnomalyBadge.svelte +1 -1
  9. package/dist/components/ApiSettings.svelte +191 -0
  10. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  11. package/dist/components/AppearanceSettings.svelte +5 -6
  12. package/dist/components/AuditLogDrawer.svelte +1 -1
  13. package/dist/components/AuditLogViewer.svelte +1 -1
  14. package/dist/components/AutoForm.svelte +50 -44
  15. package/dist/components/AutoTable.svelte +44 -39
  16. package/dist/components/Breadcrumbs.svelte +1 -0
  17. package/dist/components/ChatDialog.svelte +5 -4
  18. package/dist/components/ComboboxField.svelte +3 -3
  19. package/dist/components/CommandPalette.svelte +3 -3
  20. package/dist/components/CopilotPanel.svelte +2 -2
  21. package/dist/components/CreatePage.svelte +0 -1
  22. package/dist/components/DevTools.svelte +4 -3
  23. package/dist/components/DraggableHeader.svelte +2 -1
  24. package/dist/components/EditPage.svelte +1 -1
  25. package/dist/components/EmptyState.svelte +1 -1
  26. package/dist/components/ErrorPage.svelte +58 -0
  27. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  28. package/dist/components/FieldRenderer.svelte +9 -8
  29. package/dist/components/ForgotPasswordPage.svelte +111 -68
  30. package/dist/components/Header.svelte +7 -7
  31. package/dist/components/InferencerPanel.svelte +4 -3
  32. package/dist/components/InfiniteList.svelte +3 -4
  33. package/dist/components/IntegrationsSettings.svelte +70 -0
  34. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  35. package/dist/components/KeyboardShortcuts.svelte +3 -3
  36. package/dist/components/Layout.svelte +13 -13
  37. package/dist/components/LoginPage.svelte +207 -105
  38. package/dist/components/LoginPage.svelte.d.ts +3 -15
  39. package/dist/components/MarkdownRenderer.svelte +3 -5
  40. package/dist/components/ModalForm.svelte +1 -3
  41. package/dist/components/NotificationsSettings.svelte +115 -0
  42. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  43. package/dist/components/PageHeader.svelte +9 -7
  44. package/dist/components/PageSkeleton.svelte +4 -4
  45. package/dist/components/PasswordInput.svelte +1 -2
  46. package/dist/components/PermissionMatrix.svelte +5 -5
  47. package/dist/components/ProfilePage.svelte +1 -1
  48. package/dist/components/RegisterPage.svelte +103 -65
  49. package/dist/components/ResourceOperationsPage.svelte +153 -0
  50. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  51. package/dist/components/RolesSettings.svelte +1 -2
  52. package/dist/components/SecuritySettings.svelte +179 -0
  53. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  54. package/dist/components/SettingsPage.svelte +121 -55
  55. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  56. package/dist/components/ShowPage.svelte +13 -15
  57. package/dist/components/Sidebar.svelte +107 -58
  58. package/dist/components/SidebarItem.svelte +38 -14
  59. package/dist/components/StepsForm.svelte +1 -1
  60. package/dist/components/TaskList.svelte +1 -1
  61. package/dist/components/TaskQueueDrawer.svelte +2 -1
  62. package/dist/components/TenantSwitcher.svelte +1 -2
  63. package/dist/components/Toast.svelte +1 -0
  64. package/dist/components/TooltipButton.svelte +1 -0
  65. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  66. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  67. package/dist/components/UpdatePasswordPage.svelte +86 -48
  68. package/dist/components/VirtualTable.svelte +1 -1
  69. package/dist/components/VoiceInput.svelte +1 -0
  70. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  71. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  72. package/dist/components/account/GetStartedPage.svelte +123 -0
  73. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  74. package/dist/components/account/ImportMembersPage.svelte +143 -0
  75. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  76. package/dist/components/account/MembersStarterPage.svelte +104 -0
  77. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  78. package/dist/components/account/SecurityLogPage.svelte +108 -0
  79. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  80. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  81. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  82. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  83. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  84. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  85. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  86. package/dist/components/account/TeamMembersPage.svelte +98 -0
  87. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  88. package/dist/components/buttons/ImportButton.svelte +1 -1
  89. package/dist/components/buttons/ListButton.svelte +1 -1
  90. package/dist/components/charts/BarChart.svelte +1 -1
  91. package/dist/components/charts/LineChart.svelte +3 -3
  92. package/dist/components/charts/PieChart.svelte +2 -2
  93. package/dist/components/fieldComponentMap.js +2 -1
  94. package/dist/components/fields/ArrayField.svelte +1 -1
  95. package/dist/components/fields/MarkdownField.svelte +1 -0
  96. package/dist/components/fields/MultiSelectField.svelte +1 -1
  97. package/dist/components/fields/TagField.svelte +1 -1
  98. package/dist/components/network/TeamCrewTablePage.svelte +122 -0
  99. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  100. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  101. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  102. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  103. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  104. package/dist/components/profile/ProfileCard.svelte +216 -0
  105. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  106. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  107. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  108. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  109. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  110. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  111. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  112. package/dist/components/ui/alert/index.js +1 -0
  113. package/dist/components/ui/badge/index.js +1 -0
  114. package/dist/components/ui/button/button.svelte +1 -0
  115. package/dist/components/ui/button/index.js +1 -0
  116. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -0
  117. package/dist/components/ui/dropdown-menu/dropdown-menu.svelte +1 -1
  118. package/dist/components/ui/sheet/sheet.svelte +1 -0
  119. package/dist/components/ui/tabs/tabs.svelte +1 -0
  120. package/dist/index.d.ts +23 -0
  121. package/dist/index.js +27 -0
  122. package/dist/router-state.svelte.js +17 -0
  123. package/package.json +13 -13
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /* eslint-disable svelte/prefer-svelte-reactivity */
2
3
  import { getResources, canAccessAsync } from '@svadmin/core';
3
4
  import type { Identity, MenuItem } from '@svadmin/core';
4
5
  import { navigate } from '@svadmin/core/router';
@@ -11,13 +12,14 @@
11
12
  import SidebarItem from './SidebarItem.svelte';
12
13
 
13
14
  import { ScrollArea } from './ui/scroll-area/index.js';
14
- import * as Collapsible from './ui/collapsible/index.js';
15
15
  import { Avatar } from './ui/avatar/index.js';
16
16
  import {
17
17
  LayoutDashboard, FileText, Users, Settings, Home,
18
- ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
18
+ ChevronLeft, ChevronRight, LogOut, Sun, Moon, Palette,
19
19
  Image as ImageIcon, Layout, Folder, Type, Video,
20
- Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench
20
+ Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench,
21
+ Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Bell, CalendarDays, Package, Shield,
22
+ Barcode, FolderTree, Building2, Warehouse
21
23
  } from '@lucide/svelte';
22
24
 
23
25
  let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
@@ -37,7 +39,7 @@
37
39
  : routeMode
38
40
  );
39
41
 
40
- const resources = getResources();
42
+ const _resources = getResources();
41
43
 
42
44
  const iconMap: Record<string, typeof LayoutDashboard> = {
43
45
  'dashboard': LayoutDashboard,
@@ -62,6 +64,18 @@
62
64
  'credit-card': CreditCard,
63
65
  'book-open': BookOpen,
64
66
  'wrench': Wrench,
67
+ 'repeat': Repeat,
68
+ 'clipboard-check': ClipboardCheck,
69
+ 'sliders-horizontal': SlidersHorizontal,
70
+ 'alert-triangle': AlertTriangle,
71
+ 'bell': Bell,
72
+ 'calendar': CalendarDays,
73
+ 'package': Package,
74
+ 'shield': Shield,
75
+ 'barcode': Barcode,
76
+ 'folder-tree': FolderTree,
77
+ 'building-2': Building2,
78
+ 'warehouse': Warehouse,
65
79
  };
66
80
 
67
81
  interface NavItem {
@@ -69,6 +83,7 @@
69
83
  label: string;
70
84
  Icon: typeof LayoutDashboard;
71
85
  group?: string;
86
+ badge?: string;
72
87
  }
73
88
 
74
89
  interface NavGroup {
@@ -80,7 +95,7 @@
80
95
  let navItems = $state.raw<NavItem[]>([]);
81
96
 
82
97
  $effect(() => {
83
- const localeVal = getLocale();
98
+ const _localeVal = getLocale();
84
99
  const currentResources = getResources();
85
100
  let cancelled = false;
86
101
 
@@ -101,6 +116,7 @@
101
116
  label: r.label,
102
117
  Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
103
118
  group: r.group,
119
+ badge: typeof r.meta?.badge === 'string' ? r.meta.badge : undefined,
104
120
  });
105
121
  }
106
122
  }
@@ -132,7 +148,7 @@
132
148
 
133
149
  /** Prefetch resource data on hover for instant navigation */
134
150
  const prefetchedResources = new Set<string>();
135
- function prefetchResource(resourceName: string) {
151
+ function _prefetchResource(resourceName: string) {
136
152
  if (resourceName === '/' || prefetchedResources.has(resourceName)) return;
137
153
  prefetchedResources.add(resourceName);
138
154
  // Trigger a background fetch by navigating the browser cache
@@ -148,16 +164,14 @@
148
164
  const key = item.group ?? null;
149
165
  if (!groupMap.has(key)) {
150
166
  groupMap.set(key, []);
151
- groups.push({ name: key, items: groupMap.get(key)! });
167
+ groups.push({ name: key, items: groupMap.get(key) ?? [] });
152
168
  }
153
- groupMap.get(key)!.push(item);
169
+ (groupMap.get(key) ?? []).push(item);
154
170
  }
155
171
 
156
172
  return groups;
157
173
  });
158
174
 
159
- // Track which groups are open
160
- let openGroups = $state<Set<string>>(new Set());
161
175
  let colorPickerOpen = $state(false);
162
176
  let colorPickerRef = $state<HTMLDivElement | null>(null);
163
177
  let colorPickerOpenedAt = 0;
@@ -203,77 +217,89 @@
203
217
 
204
218
  <aside
205
219
  aria-label="Sidebar navigation"
206
- class="fixed inset-y-0 left-0 z-30 flex flex-col backdrop-blur-xl transition-all duration-300"
207
- style="background-color: var(--sidebar);"
208
- class:w-64={!collapsed}
209
- class:w-16={collapsed}
220
+ class="fixed inset-y-0 left-0 z-30 flex flex-col border-r border-border/70 bg-sidebar/95 shadow-sm shadow-slate-900/[0.03] backdrop-blur-xl transition-all duration-300"
221
+ class:w-72={!collapsed}
222
+ class:w-20={collapsed}
223
+ data-sidebar="sidebar"
210
224
  >
211
225
  <!-- Logo -->
212
- <div class="flex h-16 items-center justify-between px-4">
226
+ <div class="flex h-16 items-center justify-between border-b border-border/60 px-4">
213
227
  {#if !collapsed}
214
228
  <div class="flex items-center gap-3">
215
- <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>
216
- <span class="font-semibold text-lg tracking-tight text-sidebar-foreground">{title}</span>
229
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-sm font-bold text-primary-foreground shadow-sm shadow-primary/25">
230
+ {title.slice(0, 1).toUpperCase()}
231
+ </div>
232
+ <div class="min-w-0">
233
+ <span class="block truncate text-[0.95rem] font-semibold tracking-tight text-sidebar-foreground">{title}</span>
234
+ <span class="block truncate text-[11px] font-medium text-sidebar-foreground/50">{getLocale() === 'zh-CN' ? '管理控制台' : 'Admin Console'}</span>
235
+ </div>
217
236
  </div>
218
237
  {:else}
219
- <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>
238
+ <div class="mx-auto flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-primary text-sm font-bold text-primary-foreground shadow-sm shadow-primary/25">
239
+ {title.slice(0, 1).toUpperCase()}
240
+ </div>
220
241
  {/if}
221
242
  {#if !collapsed}
222
- <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
243
+ <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="rounded-xl text-sidebar-foreground/55 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground">
223
244
  <ChevronLeft class="h-4 w-4" />
224
245
  </TooltipButton>
225
246
  {:else}
226
- <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mt-2 mb-2 block">
247
+ <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mb-2 mt-2 block rounded-xl text-sidebar-foreground/55 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground">
227
248
  <ChevronRight class="h-4 w-4" />
228
249
  </TooltipButton>
229
250
  {/if}
230
251
  </div>
231
252
 
232
- <ScrollArea class="flex-1">
233
- <nav aria-label="Main menu" class="py-4 px-3 space-y-1">
253
+ <div class="flex min-h-0 flex-1">
254
+ <ScrollArea class="min-w-0 flex-1">
255
+ <nav aria-label="Main menu" class="space-y-5 px-3 py-5">
234
256
  {#if menu && menu.length > 0}
235
257
  <!-- Custom multi-level menu -->
236
- {#each menu as item}
258
+ {#each menu as item, _i (_i)}
237
259
  <SidebarItem {item} currentPath={path} {collapsed} depth={0} />
238
260
  {/each}
239
261
  {:else}
240
262
  <!-- Auto-generated menu from resources (fallback) -->
241
- {#each navGroups as group}
263
+ {#each navGroups as group, _i (_i)}
242
264
  {#if group.name && !collapsed}
243
- <!-- Grouped section with Collapsible -->
244
- <Collapsible.Root open={openGroups.has(group.name)} onOpenChange={(isOpen) => {
245
- const next = new Set(openGroups);
246
- if (isOpen) next.add(group.name!); else next.delete(group.name!);
247
- openGroups = next;
248
- }}>
249
- <Collapsible.Trigger
250
- 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"
265
+ <!-- Grouped section: expanded by default in desktop light sidebar mode -->
266
+ <div>
267
+ <div
268
+ class="flex w-full items-center justify-between px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-sidebar-foreground/45"
269
+ data-sidebar="group-label"
251
270
  >
252
271
  <span>{group.name}</span>
253
- <ChevronDown class="h-3 w-3 transition-transform {openGroups.has(group.name) ? 'rotate-180' : ''}" />
254
- </Collapsible.Trigger>
255
- <Collapsible.Content>
256
- <div class="mt-1 space-y-1">
257
- {#each group.items as item}
272
+ </div>
273
+ <div class="mt-1 space-y-1" data-sidebar="menu-sub">
274
+ {#each group.items as item, _i (_i)}
258
275
  {@const active = isActive(item.path)}
259
276
  <a
260
277
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
261
278
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
262
- class="flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-300
279
+ class="group relative flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-200
263
280
  {active
264
- ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
265
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
281
+ ? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
282
+ : 'text-sidebar-foreground/68 hover:bg-background/85 hover:text-sidebar-foreground'}"
283
+ data-sidebar="menu-sub-button"
284
+ data-active={active ? "true" : "false"}
266
285
  >
267
- <item.Icon class="h-4 w-4 flex-shrink-0" />
268
- <span>{item.label}</span>
286
+ {#if active && !collapsed}
287
+ <div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
288
+ {/if}
289
+ <span data-sidebar="menu-icon" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg {active ? 'bg-primary-foreground/15' : 'bg-muted/60 text-sidebar-foreground/55 group-hover:bg-primary/10 group-hover:text-primary'}">
290
+ <item.Icon class="h-4 w-4 flex-shrink-0" />
291
+ </span>
292
+ <span class="min-w-0 flex-1 truncate">{item.label}</span>
293
+ {#if item.badge}
294
+ <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>
295
+ {/if}
269
296
  </a>
270
297
  {/each}
271
- </div>
272
- </Collapsible.Content>
273
- </Collapsible.Root>
298
+ </div>
299
+ </div>
274
300
  {:else}
275
301
  <!-- Ungrouped items (flat) -->
276
- {#each group.items as item}
302
+ {#each group.items as item, _i (_i)}
277
303
  {@const active = isActive(item.path)}
278
304
  {#if collapsed}
279
305
  <Tooltip.Root>
@@ -283,10 +309,12 @@
283
309
  {...props}
284
310
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
285
311
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
286
- class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
312
+ class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
287
313
  {active
288
- ? 'bg-sidebar-accent text-sidebar-accent-foreground shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20'
289
- : 'text-sidebar-foreground/60 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground'}"
314
+ ? 'bg-primary/10 text-primary shadow-sm shadow-primary/10 ring-1 ring-primary/15'
315
+ : 'text-sidebar-foreground/60 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
316
+ data-sidebar="menu-button"
317
+ data-active={active ? "true" : "false"}
290
318
  >
291
319
  <item.Icon class="h-4 w-4 flex-shrink-0" />
292
320
  </a>
@@ -300,13 +328,23 @@
300
328
  <a
301
329
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
302
330
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
303
- class="flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
331
+ class="group relative flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
304
332
  {active
305
- ? 'bg-sidebar-accent text-sidebar-accent-foreground sidebar-nav-active shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20 translate-x-1'
306
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
333
+ ? 'bg-primary text-primary-foreground shadow-sm shadow-primary/20'
334
+ : 'text-sidebar-foreground/68 hover:bg-background/85 hover:text-sidebar-foreground'}"
335
+ data-sidebar="menu-button"
336
+ data-active={active ? "true" : "false"}
307
337
  >
308
- <item.Icon class="h-4 w-4 flex-shrink-0" />
309
- <span>{item.label}</span>
338
+ {#if active && !collapsed}
339
+ <div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
340
+ {/if}
341
+ <span data-sidebar="menu-icon" class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg {active ? 'bg-primary-foreground/15' : 'bg-muted/60 text-sidebar-foreground/55 group-hover:bg-primary/10 group-hover:text-primary'}">
342
+ <item.Icon class="h-4 w-4 flex-shrink-0" />
343
+ </span>
344
+ <span class="min-w-0 flex-1 truncate">{item.label}</span>
345
+ {#if item.badge}
346
+ <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>
347
+ {/if}
310
348
  </a>
311
349
  {/if}
312
350
  {/each}
@@ -315,16 +353,17 @@
315
353
  {/if}
316
354
  </nav>
317
355
  </ScrollArea>
356
+ </div>
318
357
 
319
358
  <!-- Footer -->
320
- <div>
359
+ <div class="border-t border-border/60 bg-sidebar/95">
321
360
  <!-- Color theme picker -->
322
361
  {#if !collapsed}
323
362
  <div class="px-3 pt-3 pb-1">
324
363
  <div class="relative" bind:this={colorPickerRef}>
325
364
  <Button
326
365
  variant="ghost"
327
- 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"
366
+ class="flex h-auto w-full items-center justify-start gap-2 rounded-xl px-2 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
328
367
  onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
329
368
  >
330
369
  <Palette class="h-4 w-4" />
@@ -336,7 +375,7 @@
336
375
  </Button>
337
376
  {#if colorPickerOpen}
338
377
  <div class="absolute bottom-full left-0 mb-1 z-50 w-40 rounded-lg border bg-popover p-1 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95">
339
- {#each getColorThemes() as ct}
378
+ {#each getColorThemes() as ct, _i (_i)}
340
379
  <button
341
380
  class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
342
381
  onclick={() => { setColorTheme(ct.id as typeof ct.id & import('@svadmin/core').ColorTheme); colorPickerOpen = false; }}
@@ -357,7 +396,7 @@
357
396
  <!-- User info -->
358
397
  {#if !collapsed && identity}
359
398
  <div class="px-3 pb-3 pt-1">
360
- <div class="flex items-center gap-3 rounded-lg p-2 hover:bg-sidebar-accent/50 transition-colors">
399
+ <div class="flex items-center gap-3 rounded-2xl border border-border/50 bg-background/65 p-2 shadow-sm shadow-slate-900/[0.02] transition-colors hover:bg-sidebar-accent/50">
361
400
  <Avatar
362
401
  src={(identity as Record<string, unknown>).avatar as string | undefined}
363
402
  alt={identity.name ?? 'User'}
@@ -414,6 +453,16 @@
414
453
  </Tooltip.Trigger>
415
454
  <Tooltip.Content side="right">{t('common.toggleTheme')}</Tooltip.Content>
416
455
  </Tooltip.Root>
456
+ <Tooltip.Root>
457
+ <Tooltip.Trigger>
458
+ {#snippet child({ props }: { props: Record<string, unknown> })}
459
+ <Button {...props} variant="ghost" size="icon" onclick={() => navigate('/settings')} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
460
+ <Settings class="h-4 w-4" />
461
+ </Button>
462
+ {/snippet}
463
+ </Tooltip.Trigger>
464
+ <Tooltip.Content side="right">{t('settings.title')}</Tooltip.Content>
465
+ </Tooltip.Root>
417
466
  <Tooltip.Root>
418
467
  <Tooltip.Trigger>
419
468
  {#snippet child({ props }: { props: Record<string, unknown> })}
@@ -1,12 +1,13 @@
1
1
  <script lang="ts">
2
2
  import type { MenuItem } from '@svadmin/core';
3
- import { t } from '@svadmin/core/i18n';
4
3
  import * as Collapsible from './ui/collapsible/index.js';
5
4
  import * as Tooltip from './ui/tooltip/index.js';
6
5
  import SidebarItem from './SidebarItem.svelte';
7
6
  import {
8
7
  LayoutDashboard, FileText, Users, Settings, Home,
9
- ChevronDown, Folder, ExternalLink, type Icon as LucideIcon,
8
+ ChevronDown, Folder, ExternalLink, Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle,
9
+ Barcode, FolderTree, Building2, Warehouse, BriefcaseBusiness, Handshake, UserRoundCheck,
10
+ CalendarCheck, KeyRound, MapPinned,
10
11
  } from '@lucide/svelte';
11
12
 
12
13
  import { formatLink } from '@svadmin/core/router';
@@ -25,9 +26,23 @@
25
26
  settings: Settings,
26
27
  home: Home,
27
28
  folder: Folder,
29
+ repeat: Repeat,
30
+ 'clipboard-check': ClipboardCheck,
31
+ 'sliders-horizontal': SlidersHorizontal,
32
+ 'alert-triangle': AlertTriangle,
33
+ 'barcode': Barcode,
34
+ 'folder-tree': FolderTree,
35
+ 'building-2': Building2,
36
+ 'warehouse': Warehouse,
37
+ 'briefcase-business': BriefcaseBusiness,
38
+ handshake: Handshake,
39
+ 'user-round-check': UserRoundCheck,
40
+ 'calendar-check': CalendarCheck,
41
+ 'key-round': KeyRound,
42
+ 'map-pinned': MapPinned,
28
43
  };
29
44
 
30
- function getIcon(name?: string | any): typeof LayoutDashboard {
45
+ function getIcon(name?: string | typeof LayoutDashboard): typeof LayoutDashboard {
31
46
  if (!name) return depth === 0 ? Settings : Folder;
32
47
  if (typeof name !== 'string') return name; // if it's already a component
33
48
  return iconMap[name] ?? Settings;
@@ -70,11 +85,13 @@
70
85
  <!-- Parent node with children: render as collapsible group -->
71
86
  <Collapsible.Root bind:open={isOpen}>
72
87
  <Collapsible.Trigger
73
- class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
88
+ class="flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
74
89
  {childActive
75
- ? 'text-sidebar-foreground bg-sidebar-accent/30'
76
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
90
+ ? 'bg-primary/10 text-primary ring-1 ring-primary/15'
91
+ : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
77
92
  style="padding-left: {12 + depth * 12}px"
93
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
94
+ data-active={childActive ? "true" : "false"}
78
95
  >
79
96
  <span class="flex items-center gap-3">
80
97
  <Icon class="h-4 w-4 flex-shrink-0" />
@@ -83,8 +100,8 @@
83
100
  <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
84
101
  </Collapsible.Trigger>
85
102
  <Collapsible.Content>
86
- <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
87
- {#each item.children as child}
103
+ <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px" data-sidebar="menu-sub">
104
+ {#each item.children as child, _i (_i)}
88
105
  <SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
89
106
  {/each}
90
107
  </div>
@@ -100,10 +117,12 @@
100
117
  href={finalHref}
101
118
  target={isExternal ? '_blank' : undefined}
102
119
  rel={isExternal ? 'noopener noreferrer' : undefined}
103
- class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
120
+ class="flex items-center justify-center rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200
104
121
  {active
105
- ? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5'
106
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
122
+ ? 'bg-primary/10 text-primary shadow-sm shadow-primary/10 ring-1 ring-primary/15'
123
+ : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
124
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
125
+ data-active={active ? "true" : "false"}
107
126
  >
108
127
  <Icon class="h-4 w-4 flex-shrink-0" />
109
128
  </a>
@@ -119,12 +138,17 @@
119
138
  href={finalHref}
120
139
  target={isExternal ? '_blank' : undefined}
121
140
  rel={isExternal ? 'noopener noreferrer' : undefined}
122
- class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
141
+ class="relative flex items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
123
142
  {active
124
- ? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5 font-semibold before:absolute before:left-0 before:top-2 before:bottom-2 before:w-[3px] before:bg-primary before:rounded-r-md overflow-hidden relative'
125
- : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
143
+ ? 'bg-primary/10 text-primary font-semibold shadow-sm shadow-primary/10 ring-1 ring-primary/15'
144
+ : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/65 hover:text-sidebar-foreground'}"
126
145
  style="padding-left: {12 + depth * 12}px"
146
+ data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
147
+ data-active={active ? "true" : "false"}
127
148
  >
149
+ {#if active && !collapsed}
150
+ <div class="absolute left-0 top-1/2 -translate-y-1/2 h-5 w-1 rounded-r-full bg-primary"></div>
151
+ {/if}
128
152
  <Icon class="h-4 w-4 flex-shrink-0" />
129
153
  <span class="flex-1">{label}</span>
130
154
  {#if isExternal}
@@ -111,7 +111,7 @@
111
111
  <!-- Step indicators -->
112
112
  <div class="space-y-3">
113
113
  <div class="flex items-center justify-between text-sm">
114
- {#each steps as step, i}
114
+ {#each steps as step, i (i)}
115
115
  <Button
116
116
  variant="ghost"
117
117
  type="button"
@@ -110,7 +110,7 @@
110
110
  </Table.Row>
111
111
  </Table.Header>
112
112
  <Table.Body>
113
- {#each resolvedTasks as task}
113
+ {#each resolvedTasks as task, _i (_i)}
114
114
  <Table.Row class="cursor-pointer hover:bg-muted/40" onclick={() => onSelect?.(task)}>
115
115
  <Table.Cell class="min-w-[240px]">
116
116
  <div class="space-y-1">
@@ -36,6 +36,7 @@
36
36
  initialTab?: TaskTab;
37
37
  } = $props();
38
38
 
39
+ // eslint-disable-next-line svelte/prefer-writable-derived
39
40
  let activeTab = $state<TaskTab>('tasks');
40
41
  let selectedTaskId = $state<string | null>(null);
41
42
  let searchQuery = $state('');
@@ -283,7 +284,7 @@
283
284
  </div>
284
285
  <Select bind:value={statusFilter} aria-label={t('task.statusFilterLabel')}>
285
286
  <option value="all">{t('task.statusAll')}</option>
286
- {#each availableStatuses as status}
287
+ {#each availableStatuses as status, _i (_i)}
287
288
  <option value={status}>{status}</option>
288
289
  {/each}
289
290
  </Select>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { ChevronsUpDown, Building2, Check } from '@lucide/svelte';
4
- import { Button } from './ui/button/index.js';
5
4
  import * as DropdownMenu from './ui/dropdown-menu/index.js';
6
5
  import type { Tenant } from '../types.js';
7
6
 
@@ -61,7 +60,7 @@
61
60
  <DropdownMenu.Separator />
62
61
  {/if}
63
62
  <div class="px-2 py-1.5 text-xs text-muted-foreground">Workspaces</div>
64
- {#each tenants as tenant}
63
+ {#each tenants as tenant, _i (_i)}
65
64
  <DropdownMenu.Item
66
65
  class="flex items-center gap-3 cursor-pointer"
67
66
  onclick={() => selectTenant(tenant.id)}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  import { Toaster, toast as sonner } from 'sonner-svelte';
3
4
  import { getResolvedTheme, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue } from '@svadmin/core';
4
5
 
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
3
  import * as Tooltip from './ui/tooltip/index.js';
3
4
  import { Button } from './ui/button/index.js';
4
5
  import type { Snippet } from 'svelte';