@djangocfg/layouts 2.1.108 → 2.1.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -138,7 +138,11 @@ import { PublicLayout, PrivateLayout, AuthLayout } from '@djangocfg/layouts';
138
138
 
139
139
  // Private page
140
140
  <PrivateLayout
141
- sidebar={{ items: menuItems }}
141
+ sidebar={{
142
+ groups: [
143
+ { label: 'Main', items: menuItems }
144
+ ]
145
+ }}
142
146
  header={{ title: 'Dashboard' }}
143
147
  >
144
148
  {children}
@@ -669,15 +673,18 @@ export default function DashboardPage() {
669
673
  return (
670
674
  <PrivateLayout
671
675
  sidebar={{
672
- items: [
673
- { label: 'Dashboard', href: '/dashboard', icon: 'LayoutDashboard' },
674
- { label: 'Settings', href: '/settings', icon: 'Settings' }
675
- ]
676
- }}
677
- header={{
678
- title: 'Dashboard',
679
- userMenu: { name: 'John Doe', email: 'john@example.com' }
676
+ groups: [
677
+ {
678
+ label: 'Main',
679
+ items: [
680
+ { label: 'Dashboard', href: '/dashboard', icon: 'LayoutDashboard' },
681
+ { label: 'Settings', href: '/settings', icon: 'Settings' }
682
+ ]
683
+ }
684
+ ],
685
+ homeHref: '/dashboard'
680
686
  }}
687
+ header={{ title: 'Dashboard' }}
681
688
  >
682
689
  <h1>Dashboard</h1>
683
690
  </PrivateLayout>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.108",
3
+ "version": "2.1.109",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,11 +74,11 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.108",
78
- "@djangocfg/centrifugo": "^2.1.108",
79
- "@djangocfg/ui-core": "^2.1.108",
80
- "@djangocfg/ui-nextjs": "^2.1.108",
81
- "@djangocfg/ui-tools": "^2.1.108",
77
+ "@djangocfg/api": "^2.1.109",
78
+ "@djangocfg/centrifugo": "^2.1.109",
79
+ "@djangocfg/ui-core": "^2.1.109",
80
+ "@djangocfg/ui-nextjs": "^2.1.109",
81
+ "@djangocfg/ui-tools": "^2.1.109",
82
82
  "@hookform/resolvers": "^5.2.0",
83
83
  "consola": "^3.4.2",
84
84
  "lucide-react": "^0.545.0",
@@ -87,7 +87,7 @@
87
87
  "p-retry": "^7.0.0",
88
88
  "react": "^19.1.0",
89
89
  "react-dom": "^19.1.0",
90
- "react-hook-form": "7.65.0",
90
+ "react-hook-form": "^7.69.0",
91
91
  "sonner": "2.0.7",
92
92
  "swr": "^2.3.7",
93
93
  "tailwindcss": "^4.1.14",
@@ -101,12 +101,12 @@
101
101
  "uuid": "^11.1.0"
102
102
  },
103
103
  "devDependencies": {
104
- "@djangocfg/api": "^2.1.108",
105
- "@djangocfg/centrifugo": "^2.1.108",
106
- "@djangocfg/typescript-config": "^2.1.108",
107
- "@djangocfg/ui-core": "^2.1.108",
108
- "@djangocfg/ui-nextjs": "^2.1.108",
109
- "@djangocfg/ui-tools": "^2.1.108",
104
+ "@djangocfg/api": "^2.1.109",
105
+ "@djangocfg/centrifugo": "^2.1.109",
106
+ "@djangocfg/typescript-config": "^2.1.109",
107
+ "@djangocfg/ui-core": "^2.1.109",
108
+ "@djangocfg/ui-nextjs": "^2.1.109",
109
+ "@djangocfg/ui-tools": "^2.1.109",
110
110
  "@types/node": "^24.7.2",
111
111
  "@types/react": "^19.1.0",
112
112
  "@types/react-dom": "^19.1.0",
@@ -59,6 +59,7 @@ import { UserMenuConfig } from '../types';
59
59
  import { PrivateContent, PrivateHeader, PrivateSidebar } from './components';
60
60
 
61
61
  import type { LucideIcon as LucideIconType } from 'lucide-react';
62
+
62
63
  export interface SidebarItem {
63
64
  label: string;
64
65
  href: string;
@@ -66,8 +67,19 @@ export interface SidebarItem {
66
67
  badge?: string | number;
67
68
  }
68
69
 
69
- export interface SidebarConfig {
70
+ export interface SidebarGroupConfig {
71
+ /** Group label displayed above items */
72
+ label: string;
73
+ /** Items in this group */
70
74
  items: SidebarItem[];
75
+ /** If true, group is only shown when it has items (for dynamic groups like extensions) */
76
+ dynamic?: boolean;
77
+ }
78
+
79
+ export interface SidebarConfig {
80
+ /** Grouped items with labels */
81
+ groups: SidebarGroupConfig[];
82
+ /** Home link href */
71
83
  homeHref?: string;
72
84
  }
73
85
 
@@ -18,7 +18,7 @@ import { cn } from '@djangocfg/ui-core/lib';
18
18
 
19
19
  import { LucideIcon } from '../../../components';
20
20
 
21
- import type { SidebarItem, SidebarConfig } from '../PrivateLayout';
21
+ import type { SidebarItem, SidebarConfig, SidebarGroupConfig } from '../PrivateLayout';
22
22
 
23
23
  interface PrivateSidebarProps {
24
24
  sidebar: SidebarConfig;
@@ -29,18 +29,71 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
29
29
  const { state, isMobile } = useSidebar();
30
30
  const homeHref = sidebar.homeHref || '/';
31
31
 
32
+ // Get all items for active detection
33
+ const allItems = React.useMemo(() => {
34
+ return sidebar.groups.flatMap((g) => g.items);
35
+ }, [sidebar.groups]);
36
+
32
37
  const isActive = (href: string) => {
33
38
  const matches = pathname === href || pathname.startsWith(href + '/');
34
39
  if (!matches) return false;
35
40
 
36
41
  // Check if there's a more specific (longer) path that also matches
37
- return !sidebar.items.some(otherItem =>
38
- otherItem.href !== href &&
39
- otherItem.href.startsWith(href + '/') &&
40
- (pathname === otherItem.href || pathname.startsWith(otherItem.href + '/'))
42
+ return !allItems.some(
43
+ (otherItem) =>
44
+ otherItem.href !== href &&
45
+ otherItem.href.startsWith(href + '/') &&
46
+ (pathname === otherItem.href ||
47
+ pathname.startsWith(otherItem.href + '/'))
48
+ );
49
+ };
50
+
51
+ // Render a single menu item
52
+ const renderMenuItem = (item: SidebarItem) => {
53
+ const active = isActive(item.href);
54
+
55
+ return (
56
+ <SidebarMenuItem key={item.href}>
57
+ <SidebarMenuButton
58
+ asChild
59
+ isActive={active}
60
+ tooltip={item.label}
61
+ size={isMobile ? 'lg' : 'default'}
62
+ >
63
+ <Link href={item.href}>
64
+ {item.icon && (
65
+ <LucideIcon
66
+ icon={typeof item.icon === 'string' ? item.icon : item.icon}
67
+ className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
68
+ />
69
+ )}
70
+ <span className={isMobile ? 'text-base' : ''}>{item.label}</span>
71
+ {item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
72
+ </Link>
73
+ </SidebarMenuButton>
74
+ </SidebarMenuItem>
41
75
  );
42
76
  };
43
77
 
78
+ // Render groups
79
+ const renderContent = () => {
80
+ return sidebar.groups.map((group) => {
81
+ // Skip dynamic groups with no items
82
+ if (group.dynamic && group.items.length === 0) {
83
+ return null;
84
+ }
85
+
86
+ return (
87
+ <SidebarGroup key={group.label}>
88
+ <SidebarGroupLabel className="font-medium text-[10px]">{group.label}</SidebarGroupLabel>
89
+ <SidebarGroupContent>
90
+ <SidebarMenu>{group.items.map(renderMenuItem)}</SidebarMenu>
91
+ </SidebarGroupContent>
92
+ </SidebarGroup>
93
+ );
94
+ });
95
+ };
96
+
44
97
  return (
45
98
  <Sidebar collapsible="icon">
46
99
  <SidebarHeader>
@@ -88,47 +141,7 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
88
141
  </div>
89
142
  </SidebarHeader>
90
143
 
91
- <SidebarContent>
92
- <SidebarGroup>
93
- <SidebarGroupContent>
94
- <SidebarMenu>
95
- {sidebar.items.map((item) => {
96
- const active = isActive(item.href);
97
-
98
- return (
99
- <SidebarMenuItem key={item.href}>
100
- <SidebarMenuButton
101
- asChild
102
- isActive={active}
103
- tooltip={item.label}
104
- size={isMobile ? 'lg' : 'default'}
105
- >
106
- <Link href={item.href}>
107
- {item.icon && (
108
- <LucideIcon
109
- icon={
110
- typeof item.icon === 'string'
111
- ? item.icon
112
- : item.icon
113
- }
114
- className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
115
- />
116
- )}
117
- <span className={isMobile ? 'text-base' : ''}>
118
- {item.label}
119
- </span>
120
- {item.badge && (
121
- <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
122
- )}
123
- </Link>
124
- </SidebarMenuButton>
125
- </SidebarMenuItem>
126
- );
127
- })}
128
- </SidebarMenu>
129
- </SidebarGroupContent>
130
- </SidebarGroup>
131
- </SidebarContent>
144
+ <SidebarContent>{renderContent()}</SidebarContent>
132
145
  </Sidebar>
133
146
  );
134
147
  }
@@ -3,5 +3,5 @@
3
3
  */
4
4
 
5
5
  export { PrivateLayout } from './PrivateLayout';
6
- export type { PrivateLayoutProps, SidebarItem, SidebarConfig, HeaderConfig } from './PrivateLayout';
6
+ export type { PrivateLayoutProps, SidebarItem, SidebarGroupConfig, SidebarConfig, HeaderConfig } from './PrivateLayout';
7
7