@svadmin/ui 0.35.0 → 0.37.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 (27) hide show
  1. package/dist/app.css +436 -66
  2. package/dist/components/AutoForm.svelte +33 -48
  3. package/dist/components/AutoTable.svelte +24 -29
  4. package/dist/components/ErrorPage.svelte +1 -1
  5. package/dist/components/Header.svelte +6 -6
  6. package/dist/components/Layout.svelte +8 -8
  7. package/dist/components/PageHeader.svelte +7 -9
  8. package/dist/components/ResourceOperationsPage.svelte +12 -14
  9. package/dist/components/SettingsPage.svelte +14 -14
  10. package/dist/components/ShowPage.svelte +9 -9
  11. package/dist/components/Sidebar.svelte +73 -115
  12. package/dist/components/SidebarItem.svelte +37 -32
  13. package/dist/components/TwoFactorAuthPage.svelte +1 -1
  14. package/dist/components/account/CompanyProfilePage.svelte +1 -1
  15. package/dist/components/account/GetStartedPage.svelte +1 -1
  16. package/dist/components/account/ImportMembersPage.svelte +1 -1
  17. package/dist/components/account/MembersStarterPage.svelte +1 -1
  18. package/dist/components/account/SecurityLogPage.svelte +29 -2
  19. package/dist/components/account/SettingsEnterprisePage.svelte +1 -1
  20. package/dist/components/account/SettingsPlainPage.svelte +1 -1
  21. package/dist/components/account/SettingsSidebarPage.svelte +1 -1
  22. package/dist/components/account/TeamMembersPage.svelte +1 -1
  23. package/dist/components/network/TeamCrewTablePage.svelte +35 -2
  24. package/dist/components/network/UserCardsNFTPage.svelte +1 -1
  25. package/dist/components/profile/PublicProfilePage.svelte +1 -1
  26. package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
  27. package/package.json +2 -2
@@ -38,10 +38,10 @@
38
38
  </PageHeader>
39
39
 
40
40
  {#if query.isLoading}
41
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
42
- <Card.Content class="space-y-3 p-4 sm:p-5">
41
+ <Card.Root class="overflow-hidden border-border/40 shadow-sm">
42
+ <Card.Content class="p-0">
43
43
  {#each showFields.slice(0, 6) as _, i (i)}
44
- <div class="flex flex-col rounded-2xl border border-border/50 bg-muted/20 px-4 py-3 sm:flex-row sm:py-4">
44
+ <div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
45
45
  <Skeleton class="h-4 w-1/2 sm:w-1/4" />
46
46
  <Skeleton class="h-4 w-3/4 sm:w-2/5 mt-1 sm:mt-0 sm:ml-auto" />
47
47
  </div>
@@ -49,14 +49,14 @@
49
49
  </Card.Content>
50
50
  </Card.Root>
51
51
  {:else if query.data?.data}
52
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
53
- <Card.Content class="space-y-3 p-4 sm:p-5">
52
+ <Card.Root class="overflow-hidden border-border/40 shadow-sm">
53
+ <Card.Content class="p-0">
54
54
  {#each showFields as field, i (i)}
55
55
  {@const value = ((query.data as { data: Record<string, unknown> }).data as Record<string, unknown>)[field.key]}
56
56
  {@const DisplayComponent = getDisplayComponent(field.type)}
57
- <div class="flex flex-col rounded-2xl border border-border/55 bg-background/70 px-4 py-3 transition-colors hover:bg-muted/20 sm:flex-row sm:items-start sm:py-4 {i % 2 === 1 ? 'bg-muted/15' : ''}">
58
- <div class="mb-1 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted-foreground sm:mb-0 sm:w-1/3">{field.label}</div>
59
- <div class="text-sm text-foreground sm:w-2/3">
57
+ <div class="flex flex-col sm:flex-row px-4 sm:px-6 py-3 sm:py-4 {i % 2 === 1 ? 'bg-muted/20' : ''}">
58
+ <div class="sm:w-1/3 text-xs sm:text-sm font-medium text-muted-foreground mb-1 sm:mb-0">{field.label}</div>
59
+ <div class="sm:w-2/3 text-sm text-foreground">
60
60
  {#if DisplayComponent && value != null}
61
61
  <DisplayComponent
62
62
  {value}
@@ -72,7 +72,7 @@
72
72
  </Card.Content>
73
73
  </Card.Root>
74
74
  {:else}
75
- <Card.Root class="overflow-hidden rounded-3xl border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03]">
75
+ <Card.Root class="overflow-hidden border-border/40 shadow-sm">
76
76
  <Card.Content class="p-8 text-center">
77
77
  <p class="text-muted-foreground">{t('common.noData')}</p>
78
78
  {#if query.isError}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable svelte/prefer-svelte-reactivity */
3
2
  import { getResources, canAccessAsync } from '@svadmin/core';
4
3
  import type { Identity, MenuItem } from '@svadmin/core';
5
4
  import { navigate } from '@svadmin/core/router';
@@ -12,14 +11,15 @@
12
11
  import SidebarItem from './SidebarItem.svelte';
13
12
 
14
13
  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, LogOut, Sun, Moon, Palette,
18
+ ChevronLeft, ChevronRight, ChevronDown, LogOut, Sun, Moon, Palette,
19
19
  Image as ImageIcon, Layout, Folder, Type, Video,
20
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
+ Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle, Mail, Send, ShoppingBag, Briefcase, Building2,
22
+ Calendar, Bell, Clock, Trash2, Shield
23
23
  } from '@lucide/svelte';
24
24
 
25
25
  let { collapsed, identity, title, onToggle, onLogout, menu, routeMode = 'auto' }: {
@@ -39,11 +39,10 @@
39
39
  : routeMode
40
40
  );
41
41
 
42
- const _resources = getResources();
43
-
44
42
  const iconMap: Record<string, typeof LayoutDashboard> = {
45
43
  'dashboard': LayoutDashboard,
46
44
  'posts': FileText,
45
+ 'file': FileText,
47
46
  'users': Users,
48
47
  'settings': Settings,
49
48
  'home': Home,
@@ -68,14 +67,16 @@
68
67
  'clipboard-check': ClipboardCheck,
69
68
  'sliders-horizontal': SlidersHorizontal,
70
69
  'alert-triangle': AlertTriangle,
70
+ 'mail': Mail,
71
+ 'send': Send,
72
+ 'shopping-bag': ShoppingBag,
73
+ 'briefcase': Briefcase,
74
+ 'building': Building2,
75
+ 'calendar': Calendar,
71
76
  'bell': Bell,
72
- 'calendar': CalendarDays,
73
- 'package': Package,
77
+ 'clock': Clock,
78
+ 'trash': Trash2,
74
79
  'shield': Shield,
75
- 'barcode': Barcode,
76
- 'folder-tree': FolderTree,
77
- 'building-2': Building2,
78
- 'warehouse': Warehouse,
79
80
  };
80
81
 
81
82
  interface NavItem {
@@ -83,7 +84,6 @@
83
84
  label: string;
84
85
  Icon: typeof LayoutDashboard;
85
86
  group?: string;
86
- badge?: string;
87
87
  }
88
88
 
89
89
  interface NavGroup {
@@ -95,7 +95,7 @@
95
95
  let navItems = $state.raw<NavItem[]>([]);
96
96
 
97
97
  $effect(() => {
98
- const _localeVal = getLocale();
98
+ getLocale();
99
99
  const currentResources = getResources();
100
100
  let cancelled = false;
101
101
 
@@ -116,7 +116,6 @@
116
116
  label: r.label,
117
117
  Icon: (typeof r.icon === 'string' ? iconMap[r.icon] : r.icon) ?? iconMap[r.name] ?? Settings,
118
118
  group: r.group,
119
- badge: typeof r.meta?.badge === 'string' ? r.meta.badge : undefined,
120
119
  });
121
120
  }
122
121
  }
@@ -146,15 +145,6 @@
146
145
  return path === itemPath || path.startsWith(itemPath + "/");
147
146
  }
148
147
 
149
- /** Prefetch resource data on hover for instant navigation */
150
- const prefetchedResources = new Set<string>();
151
- function _prefetchResource(resourceName: string) {
152
- if (resourceName === '/' || prefetchedResources.has(resourceName)) return;
153
- prefetchedResources.add(resourceName);
154
- // Trigger a background fetch by navigating the browser cache
155
- // The actual prefetch happens via the #key block re-creating useList in AdminApp
156
- }
157
-
158
148
  // Group nav items by group field (null = ungrouped)
159
149
  const navGroups = $derived.by((): NavGroup[] => {
160
150
  const groups: NavGroup[] = [];
@@ -172,6 +162,8 @@
172
162
  return groups;
173
163
  });
174
164
 
165
+ // Track which groups are open
166
+ let openGroups = $state<Set<string>>(new Set());
175
167
  let colorPickerOpen = $state(false);
176
168
  let colorPickerRef = $state<HTMLDivElement | null>(null);
177
169
  let colorPickerOpenedAt = 0;
@@ -195,6 +187,7 @@
195
187
 
196
188
  const pyClass = $derived(density === 'compact' ? 'py-1.5' : 'py-2.5');
197
189
  const pyClassGroupItem = $derived(density === 'compact' ? 'py-1.5' : 'py-2');
190
+ const footerIconClass = 'h-9 w-9 rounded-lg text-sidebar-foreground/60 transition-all duration-200 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground';
198
191
 
199
192
  // Click-outside to close color picker
200
193
  $effect(() => {
@@ -217,42 +210,34 @@
217
210
 
218
211
  <aside
219
212
  aria-label="Sidebar navigation"
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"
213
+ class="fixed inset-y-0 left-0 z-30 flex flex-col backdrop-blur-xl transition-all duration-300"
214
+ style="background-color: var(--sidebar);"
215
+ class:w-64={!collapsed}
216
+ class:w-16={collapsed}
224
217
  >
225
218
  <!-- Logo -->
226
- <div class="flex h-16 items-center justify-between border-b border-border/60 px-4">
219
+ <div class="flex h-16 items-center justify-between px-4">
227
220
  {#if !collapsed}
228
221
  <div class="flex items-center gap-3">
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>
222
+ <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>
223
+ <span class="font-semibold text-lg tracking-tight text-sidebar-foreground">{title}</span>
236
224
  </div>
237
225
  {:else}
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>
226
+ <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>
241
227
  {/if}
242
228
  {#if !collapsed}
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">
229
+ <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle}>
244
230
  <ChevronLeft class="h-4 w-4" />
245
231
  </TooltipButton>
246
232
  {:else}
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">
233
+ <TooltipButton tooltip={t('common.toggleSidebar')} variant="ghost" size="icon-sm" onclick={onToggle} class="mx-auto mt-2 mb-2 block">
248
234
  <ChevronRight class="h-4 w-4" />
249
235
  </TooltipButton>
250
236
  {/if}
251
237
  </div>
252
238
 
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">
239
+ <ScrollArea class="flex-1">
240
+ <nav aria-label="Main menu" class="py-4 px-3 space-y-1">
256
241
  {#if menu && menu.length > 0}
257
242
  <!-- Custom multi-level menu -->
258
243
  {#each menu as item, _i (_i)}
@@ -262,41 +247,37 @@
262
247
  <!-- Auto-generated menu from resources (fallback) -->
263
248
  {#each navGroups as group, _i (_i)}
264
249
  {#if group.name && !collapsed}
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"
250
+ <!-- Grouped section with Collapsible -->
251
+ <Collapsible.Root open={openGroups.has(group.name)} onOpenChange={(isOpen) => {
252
+ const next = new Set(openGroups);
253
+ if (isOpen) next.add(group.name as string); else next.delete(group.name as string);
254
+ openGroups = next;
255
+ }}>
256
+ <Collapsible.Trigger
257
+ 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"
270
258
  >
271
259
  <span>{group.name}</span>
272
- </div>
273
- <div class="mt-1 space-y-1" data-sidebar="menu-sub">
260
+ <ChevronDown class="h-3 w-3 transition-transform {openGroups.has(group.name) ? 'rotate-180' : ''}" />
261
+ </Collapsible.Trigger>
262
+ <Collapsible.Content>
263
+ <div class="mt-1 space-y-1">
274
264
  {#each group.items as item, _i (_i)}
275
265
  {@const active = isActive(item.path)}
276
266
  <a
277
267
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
278
268
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
279
- class="group relative flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-200
269
+ class="flex items-center gap-3 rounded-xl px-3 {pyClassGroupItem} text-sm font-medium transition-all duration-300
280
270
  {active
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"}
271
+ ? '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'
272
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
285
273
  >
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}
274
+ <item.Icon class="h-4 w-4 flex-shrink-0" />
275
+ <span>{item.label}</span>
296
276
  </a>
297
277
  {/each}
298
- </div>
299
- </div>
278
+ </div>
279
+ </Collapsible.Content>
280
+ </Collapsible.Root>
300
281
  {:else}
301
282
  <!-- Ungrouped items (flat) -->
302
283
  {#each group.items as item, _i (_i)}
@@ -309,12 +290,10 @@
309
290
  {...props}
310
291
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
311
292
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
312
- class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
293
+ class="flex items-center justify-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
313
294
  {active
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"}
295
+ ? 'bg-sidebar-accent text-sidebar-accent-foreground shadow-[0_2px_12px_rgb(0,0,0,0.08)] ring-1 ring-border/20'
296
+ : 'text-sidebar-foreground/60 hover:bg-sidebar-accent/40 hover:text-sidebar-foreground'}"
318
297
  >
319
298
  <item.Icon class="h-4 w-4 flex-shrink-0" />
320
299
  </a>
@@ -328,23 +307,13 @@
328
307
  <a
329
308
  href={effectiveRouteMode === 'hash' ? `#${item.path}` : item.path}
330
309
  onclick={(e) => { e.preventDefault(); navigate(item.path); }}
331
- class="group relative flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-200
310
+ class="flex items-center gap-3 rounded-xl px-3 {pyClass} text-sm font-medium transition-all duration-300
332
311
  {active
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"}
312
+ ? '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'
313
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
337
314
  >
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}
315
+ <item.Icon class="h-4 w-4 flex-shrink-0" />
316
+ <span>{item.label}</span>
348
317
  </a>
349
318
  {/if}
350
319
  {/each}
@@ -353,17 +322,16 @@
353
322
  {/if}
354
323
  </nav>
355
324
  </ScrollArea>
356
- </div>
357
325
 
358
326
  <!-- Footer -->
359
- <div class="border-t border-border/60 bg-sidebar/95">
327
+ <div>
360
328
  <!-- Color theme picker -->
361
329
  {#if !collapsed}
362
330
  <div class="px-3 pt-3 pb-1">
363
331
  <div class="relative" bind:this={colorPickerRef}>
364
332
  <Button
365
333
  variant="ghost"
366
- class="flex h-auto w-full items-center justify-start gap-2 rounded-xl px-2 py-1.5 text-sm text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
334
+ 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"
367
335
  onclick={() => { if (!colorPickerOpen) colorPickerOpenedAt = Date.now(); colorPickerOpen = !colorPickerOpen; }}
368
336
  >
369
337
  <Palette class="h-4 w-4" />
@@ -395,8 +363,8 @@
395
363
 
396
364
  <!-- User info -->
397
365
  {#if !collapsed && identity}
398
- <div class="px-3 pb-3 pt-1">
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">
366
+ <div class="px-3 pb-3 pt-1 transition-all duration-200">
367
+ <div class="flex items-center gap-3 rounded-lg p-2 transition-colors hover:bg-sidebar-accent/50">
400
368
  <Avatar
401
369
  src={(identity as Record<string, unknown>).avatar as string | undefined}
402
370
  alt={identity.name ?? 'User'}
@@ -408,31 +376,31 @@
408
376
  <p class="truncate text-[10px] text-sidebar-foreground/50">{((identity as Record<string, unknown>).role || (identity as Record<string, unknown>).roleName) ?? 'User'}</p>
409
377
  </div>
410
378
  </div>
411
- <div class="flex items-center justify-between px-1 mt-1">
412
- <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
379
+ <div class="mt-1 grid grid-cols-4 place-items-center gap-1 px-1">
380
+ <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon-sm" onclick={toggleLocale} class={footerIconClass}>
413
381
  <span class="text-xs font-bold">{localeLabel}</span>
414
382
  </TooltipButton>
415
- <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
383
+ <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon-sm" onclick={toggleTheme} class={footerIconClass}>
416
384
  {#if getResolvedTheme() === 'dark'}
417
385
  <Sun class="h-4 w-4" />
418
386
  {:else}
419
387
  <Moon class="h-4 w-4" />
420
388
  {/if}
421
389
  </TooltipButton>
422
- <TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class="text-sidebar-foreground/60 hover:text-sidebar-foreground">
390
+ <TooltipButton tooltip={t('settings.title')} variant="ghost" size="icon-sm" onclick={() => navigate('/settings')} class={footerIconClass}>
423
391
  <Settings class="h-4 w-4" />
424
392
  </TooltipButton>
425
- <TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="text-sidebar-foreground/60 hover:text-destructive">
393
+ <TooltipButton tooltip={t('common.logout')} variant="ghost" size="icon-sm" onclick={onLogout} class="{footerIconClass} hover:text-destructive">
426
394
  <LogOut class="h-4 w-4" />
427
395
  </TooltipButton>
428
396
  </div>
429
397
  </div>
430
398
  {:else if collapsed}
431
- <div class="px-1 py-3 space-y-1">
399
+ <div class="flex flex-col items-center gap-1 px-3 py-3 transition-all duration-200">
432
400
  <Tooltip.Root>
433
401
  <Tooltip.Trigger>
434
402
  {#snippet child({ props }: { props: Record<string, unknown> })}
435
- <Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
403
+ <Button {...props} variant="ghost" size="icon" onclick={toggleLocale} class={footerIconClass}>
436
404
  <span class="text-xs font-bold">{localeLabel}</span>
437
405
  </Button>
438
406
  {/snippet}
@@ -442,7 +410,7 @@
442
410
  <Tooltip.Root>
443
411
  <Tooltip.Trigger>
444
412
  {#snippet child({ props }: { props: Record<string, unknown> })}
445
- <Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class="w-full text-sidebar-foreground/60 hover:text-sidebar-foreground">
413
+ <Button {...props} variant="ghost" size="icon" onclick={toggleTheme} class={footerIconClass}>
446
414
  {#if getResolvedTheme() === 'dark'}
447
415
  <Sun class="h-4 w-4" />
448
416
  {:else}
@@ -456,17 +424,7 @@
456
424
  <Tooltip.Root>
457
425
  <Tooltip.Trigger>
458
426
  {#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>
466
- <Tooltip.Root>
467
- <Tooltip.Trigger>
468
- {#snippet child({ props }: { props: Record<string, unknown> })}
469
- <Button {...props} variant="ghost" size="icon" onclick={onLogout} class="w-full text-sidebar-foreground/60 hover:text-destructive">
427
+ <Button {...props} variant="ghost" size="icon" onclick={onLogout} class="{footerIconClass} hover:text-destructive">
470
428
  <LogOut class="h-4 w-4" />
471
429
  </Button>
472
430
  {/snippet}
@@ -475,11 +433,11 @@
475
433
  </Tooltip.Root>
476
434
  </div>
477
435
  {:else}
478
- <div class="flex gap-1 p-3">
479
- <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class="flex-1 text-sidebar-foreground/60 hover:text-sidebar-foreground">
436
+ <div class="flex justify-center gap-2 p-3 transition-all duration-200">
437
+ <TooltipButton tooltip={t('common.switchLanguage')} variant="ghost" size="icon" onclick={toggleLocale} class={footerIconClass}>
480
438
  <span class="text-xs font-bold">{localeLabel}</span>
481
439
  </TooltipButton>
482
- <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class="flex-1 text-sidebar-foreground/60 hover:text-sidebar-foreground">
440
+ <TooltipButton tooltip={t('common.toggleTheme')} variant="ghost" size="icon" onclick={toggleTheme} class={footerIconClass}>
483
441
  {#if getResolvedTheme() === 'dark'}
484
442
  <Sun class="h-4 w-4" />
485
443
  {:else}
@@ -6,8 +6,8 @@
6
6
  import {
7
7
  LayoutDashboard, FileText, Users, Settings, Home,
8
8
  ChevronDown, Folder, ExternalLink, Repeat, ClipboardCheck, SlidersHorizontal, AlertTriangle,
9
- Barcode, FolderTree, Building2, Warehouse, BriefcaseBusiness, Handshake, UserRoundCheck,
10
- CalendarCheck, KeyRound, MapPinned,
9
+ Mail, Send, ShoppingBag, Briefcase, Building2, Calendar, Bell, Clock, Trash2, Palette, Layout,
10
+ Download, ListTodo, TrendingUp, Sparkles, Images, Bot, Key, KeyRound, CreditCard, BookOpen, Wrench, Shield,
11
11
  } from '@lucide/svelte';
12
12
 
13
13
  import { formatLink } from '@svadmin/core/router';
@@ -22,6 +22,7 @@
22
22
  const iconMap: Record<string, typeof LayoutDashboard> = {
23
23
  dashboard: LayoutDashboard,
24
24
  posts: FileText,
25
+ file: FileText,
25
26
  users: Users,
26
27
  settings: Settings,
27
28
  home: Home,
@@ -30,21 +31,34 @@
30
31
  'clipboard-check': ClipboardCheck,
31
32
  'sliders-horizontal': SlidersHorizontal,
32
33
  '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,
34
+ mail: Mail,
35
+ send: Send,
36
+ 'shopping-bag': ShoppingBag,
37
+ briefcase: Briefcase,
38
+ building: Building2,
39
+ calendar: Calendar,
40
+ bell: Bell,
41
+ clock: Clock,
42
+ trash: Trash2,
43
+ palette: Palette,
44
+ layout: Layout,
45
+ download: Download,
46
+ 'list-todo': ListTodo,
47
+ 'trending-up': TrendingUp,
48
+ sparkles: Sparkles,
49
+ images: Images,
50
+ bot: Bot,
51
+ key: Key,
41
52
  'key-round': KeyRound,
42
- 'map-pinned': MapPinned,
53
+ 'credit-card': CreditCard,
54
+ 'book-open': BookOpen,
55
+ wrench: Wrench,
56
+ shield: Shield,
43
57
  };
44
58
 
45
- function getIcon(name?: string | typeof LayoutDashboard): typeof LayoutDashboard {
59
+ function getIcon(name?: unknown): typeof LayoutDashboard {
46
60
  if (!name) return depth === 0 ? Settings : Folder;
47
- if (typeof name !== 'string') return name; // if it's already a component
61
+ if (typeof name !== 'string') return name as typeof LayoutDashboard;
48
62
  return iconMap[name] ?? Settings;
49
63
  }
50
64
 
@@ -85,13 +99,11 @@
85
99
  <!-- Parent node with children: render as collapsible group -->
86
100
  <Collapsible.Root bind:open={isOpen}>
87
101
  <Collapsible.Trigger
88
- class="flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
102
+ class="flex w-full items-center justify-between rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
89
103
  {childActive
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'}"
104
+ ? 'text-sidebar-foreground bg-sidebar-accent/30'
105
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
92
106
  style="padding-left: {12 + depth * 12}px"
93
- data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
94
- data-active={childActive ? "true" : "false"}
95
107
  >
96
108
  <span class="flex items-center gap-3">
97
109
  <Icon class="h-4 w-4 flex-shrink-0" />
@@ -100,7 +112,7 @@
100
112
  <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
101
113
  </Collapsible.Trigger>
102
114
  <Collapsible.Content>
103
- <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px" data-sidebar="menu-sub">
115
+ <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
104
116
  {#each item.children as child, _i (_i)}
105
117
  <SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
106
118
  {/each}
@@ -117,12 +129,10 @@
117
129
  href={finalHref}
118
130
  target={isExternal ? '_blank' : undefined}
119
131
  rel={isExternal ? 'noopener noreferrer' : undefined}
120
- class="flex items-center justify-center rounded-xl px-3 py-2.5 text-sm font-medium transition-all duration-200
132
+ class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
121
133
  {active
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"}
134
+ ? 'bg-card text-sidebar-primary shadow-sm ring-1 ring-foreground/5'
135
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
126
136
  >
127
137
  <Icon class="h-4 w-4 flex-shrink-0" />
128
138
  </a>
@@ -138,17 +148,12 @@
138
148
  href={finalHref}
139
149
  target={isExternal ? '_blank' : undefined}
140
150
  rel={isExternal ? 'noopener noreferrer' : undefined}
141
- class="relative flex items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium transition-all duration-200
151
+ class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
142
152
  {active
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'}"
153
+ ? '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'
154
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
145
155
  style="padding-left: {12 + depth * 12}px"
146
- data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
147
- data-active={active ? "true" : "false"}
148
156
  >
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}
152
157
  <Icon class="h-4 w-4 flex-shrink-0" />
153
158
  <span class="flex-1">{label}</span>
154
159
  {#if isExternal}
@@ -79,7 +79,7 @@
79
79
  }
80
80
  </script>
81
81
 
82
- <div class="flex min-h-[70vh] items-center justify-center p-4">
82
+ <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="auth-2fa">
83
83
  <div class="w-full max-w-lg space-y-6">
84
84
  {#if currentStep === 'intro' && !is2faEnabled}
85
85
  <!-- Intro -->
@@ -21,7 +21,7 @@
21
21
  }
22
22
  </script>
23
23
 
24
- <div class="space-y-6">
24
+ <div class="space-y-6" data-svadmin-content-page="account">
25
25
  <div>
26
26
  <h2 class="text-xl font-semibold text-foreground">{t('account.companyProfile')}</h2>
27
27
  <p class="mt-1 text-sm text-muted-foreground">{t('account.companyProfileDescription')}</p>
@@ -47,7 +47,7 @@
47
47
  }
48
48
  </script>
49
49
 
50
- <div class="space-y-6">
50
+ <div class="space-y-6" data-svadmin-content-page="account">
51
51
  <div class="flex items-start justify-between gap-4">
52
52
  <div>
53
53
  <h2 class="text-xl font-semibold text-foreground">{t('account.getStarted')}</h2>
@@ -59,7 +59,7 @@
59
59
  }
60
60
  </script>
61
61
 
62
- <div class="space-y-6">
62
+ <div class="space-y-6" data-svadmin-content-page="account">
63
63
  <div>
64
64
  <h2 class="text-xl font-semibold text-foreground">{t('account.importMembers')}</h2>
65
65
  <p class="mt-1 text-sm text-muted-foreground">{t('account.importMembersDescription')}</p>
@@ -43,7 +43,7 @@
43
43
  const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
44
44
  </script>
45
45
 
46
- <div class="space-y-6">
46
+ <div class="space-y-6" data-svadmin-content-page="account">
47
47
  <div class="flex items-start justify-between gap-4">
48
48
  <div>
49
49
  <h2 class="text-xl font-semibold text-foreground">{t('account.membersStarter')}</h2>