@svadmin/ui 0.36.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.
@@ -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
 
@@ -64,41 +78,16 @@
64
78
  return menuItem.children?.some((c: MenuItem) => hasActiveChild(c)) ?? false;
65
79
  }
66
80
 
67
- function getItemBadge(menuItem: MenuItem): string | undefined {
68
- const meta = menuItem.meta as (MenuItem['meta'] & { badge?: unknown }) | undefined;
69
- return typeof meta?.badge === 'string' ? meta.badge : undefined;
70
- }
71
-
72
- function aggregateBadges(menuItems: MenuItem[] | undefined): string | undefined {
73
- const total = menuItems?.reduce((sum, menuItem) => {
74
- const ownBadge = Number(getItemBadge(menuItem));
75
- const childBadge = Number(aggregateBadges(menuItem.children));
76
- return sum + (Number.isFinite(ownBadge) ? ownBadge : 0) + (Number.isFinite(childBadge) ? childBadge : 0);
77
- }, 0) ?? 0;
78
-
79
- return total > 0 ? String(total) : undefined;
80
- }
81
-
82
81
  const hasChildren = $derived(item.children && item.children.length > 0);
83
82
  const active = $derived(isActive(item.href));
84
83
  const childActive = $derived(hasActiveChild(item));
85
84
  const isExternal = $derived(item.target === '_blank' || item.href?.startsWith('http'));
86
- const badge = $derived(getItemBadge(item));
87
- const childrenBadge = $derived(aggregateBadges(item.children));
88
85
 
89
86
  const finalHref = $derived(isExternal ? item.href : (item.href ? formatLink(item.href) : undefined));
90
- // Auto-open the active branch once, then respect manual collapse/expand.
91
- let isOpen = $state(false);
92
- let touchedOpen = $state(false);
93
87
 
94
- function setOpen(nextOpen: boolean) {
95
- touchedOpen = true;
96
- isOpen = nextOpen;
97
- }
98
-
99
- $effect(() => {
100
- if (childActive && !touchedOpen && !isOpen) isOpen = true;
101
- });
88
+ // Track open state — auto-open if a child is active
89
+ let isOpen = $state(false);
90
+ $effect(() => { if (childActive) isOpen = true; });
102
91
 
103
92
  const Icon = $derived(getIcon(item.icon));
104
93
  const label = $derived(getLabel(item));
@@ -108,33 +97,22 @@
108
97
  <!-- hidden item, render nothing -->
109
98
  {:else if hasChildren && !collapsed}
110
99
  <!-- Parent node with children: render as collapsible group -->
111
- <Collapsible.Root open={isOpen} onOpenChange={setOpen}>
100
+ <Collapsible.Root bind:open={isOpen}>
112
101
  <Collapsible.Trigger
113
- class="flex w-full items-center justify-between rounded-xl px-3 {depth === 1 ? 'py-1.5' : '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
114
103
  {childActive
115
- ? 'text-primary font-semibold'
116
- : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 hover:text-sidebar-foreground'}"
117
- style="padding-left: {depth === 0 ? 12 : 4 + (depth - 1) * 12}px"
118
- data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
119
- data-active={childActive ? "true" : "false"}
104
+ ? 'text-sidebar-foreground bg-sidebar-accent/30'
105
+ : 'text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground'}"
106
+ style="padding-left: {12 + depth * 12}px"
120
107
  >
121
108
  <span class="flex items-center gap-3">
122
109
  <Icon class="h-4 w-4 flex-shrink-0" />
123
110
  <span>{label}</span>
124
111
  </span>
125
- <span class="ml-2 flex items-center gap-2">
126
- {#if childrenBadge}
127
- <span class="rounded-full {childActive ? 'bg-primary/12 text-primary' : 'bg-sidebar-accent text-sidebar-foreground/60'} px-2 py-0.5 text-[10px] font-semibold">{childrenBadge}</span>
128
- {/if}
129
- <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
130
- </span>
112
+ <ChevronDown class="h-3.5 w-3.5 transition-transform duration-200 {isOpen ? 'rotate-180' : ''}" />
131
113
  </Collapsible.Trigger>
132
114
  <Collapsible.Content>
133
- <div
134
- class="mt-0.5 {depth >= 1 ? 'divide-y divide-border/45' : 'space-y-0'}"
135
- style="padding-left: {depth > 0 ? 0 : 0}px"
136
- data-sidebar="menu-sub"
137
- >
115
+ <div class="mt-0.5 space-y-0.5" style="padding-left: {depth > 0 ? 0 : 0}px">
138
116
  {#each item.children as child, _i (_i)}
139
117
  <SidebarItem item={child} {currentPath} {collapsed} depth={depth + 1} />
140
118
  {/each}
@@ -151,12 +129,10 @@
151
129
  href={finalHref}
152
130
  target={isExternal ? '_blank' : undefined}
153
131
  rel={isExternal ? 'noopener noreferrer' : undefined}
154
- class="mx-auto flex h-10 w-10 items-center justify-center text-sm font-medium transition-colors duration-200
132
+ class="flex items-center justify-center rounded-lg px-3 py-2.5 text-sm font-medium transition-all duration-200
155
133
  {active
156
- ? 'text-primary'
157
- : 'text-sidebar-foreground/68 hover:text-sidebar-foreground'}"
158
- data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
159
- 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'}"
160
136
  >
161
137
  <Icon class="h-4 w-4 flex-shrink-0" />
162
138
  </a>
@@ -172,21 +148,16 @@
172
148
  href={finalHref}
173
149
  target={isExternal ? '_blank' : undefined}
174
150
  rel={isExternal ? 'noopener noreferrer' : undefined}
175
- class="relative flex items-center gap-3 px-3 {depth === 1 ? 'py-1.5' : 'py-2'} text-sm font-medium transition-all duration-200 {depth >= 2 ? 'first:rounded-t-xl last:rounded-b-xl' : 'rounded-xl'}
151
+ class="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200
176
152
  {active
177
- ? 'text-primary font-semibold'
178
- : 'text-sidebar-foreground/68 hover:bg-sidebar-accent/45 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'}"
179
155
  style="padding-left: {12 + depth * 12}px"
180
- data-sidebar={depth === 0 ? "menu-button" : "menu-sub-button"}
181
- data-active={active ? "true" : "false"}
182
156
  >
183
157
  <Icon class="h-4 w-4 flex-shrink-0" />
184
158
  <span class="flex-1">{label}</span>
185
159
  {#if isExternal}
186
160
  <ExternalLink class="h-3 w-3 opacity-50" />
187
161
  {/if}
188
- {#if badge}
189
- <span class="rounded-full {active ? 'bg-primary/12 text-primary' : 'bg-sidebar-accent text-sidebar-foreground/60'} px-2 py-0.5 text-[10px] font-semibold">{badge}</span>
190
- {/if}
191
162
  </a>
192
163
  {/if}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -33,7 +33,7 @@
33
33
  "./components/*": "./dist/components/*"
34
34
  },
35
35
  "peerDependencies": {
36
- "@svadmin/core": "^0.29.0",
36
+ "@svadmin/core": "^0.30.0",
37
37
  "@tanstack/svelte-query": "^6.1.29",
38
38
  "highlight.js": "^11.11.1",
39
39
  "isomorphic-dompurify": "^3.15.0",