@djangocfg/nextjs 2.1.107 → 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.
@@ -14,6 +14,7 @@ interface RouteMetadata {
14
14
  group?: string;
15
15
  order?: number;
16
16
  show?: boolean;
17
+ extensionName?: string;
17
18
  priority?: number;
18
19
  changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
19
20
  noindex?: boolean;
@@ -22,6 +23,20 @@ interface RouteDefinition {
22
23
  path: string;
23
24
  metadata: RouteMetadata;
24
25
  }
26
+ interface NavGroup {
27
+ /** Unique group identifier (matches RouteMetadata.group) */
28
+ id: string;
29
+ /** Display label for the group */
30
+ label: string;
31
+ /** Sort order */
32
+ order: number;
33
+ /** If true, group is only shown when it has items (e.g., extensions) */
34
+ dynamic?: boolean;
35
+ }
36
+ interface NavGroupWithRoutes extends NavGroup {
37
+ /** Routes in this group (populated at runtime) */
38
+ routes: RouteDefinition[];
39
+ }
25
40
  interface MenuItem {
26
41
  path: string;
27
42
  label: string;
@@ -88,5 +103,24 @@ declare function isActive(current: string, target: string, allRoutes?: RouteDefi
88
103
  * Filter and convert routes to menu items
89
104
  */
90
105
  declare function routesToMenuItems(routes: RouteDefinition[], groupName: string): MenuItem[];
106
+ /**
107
+ * Group routes by navigation groups
108
+ *
109
+ * @param routes - Array of route definitions
110
+ * @param groups - Array of navigation group definitions
111
+ * @returns Array of groups with their routes, filtered by dynamic flag
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const navGroups = [
116
+ * { id: 'dashboard', label: 'Dashboard', order: 1 },
117
+ * { id: 'extensions', label: 'Extensions', order: 2, dynamic: true },
118
+ * ];
119
+ *
120
+ * const groupedRoutes = groupRoutesByNavGroups(allRoutes, navGroups);
121
+ * // Returns groups with routes, hiding dynamic groups with no routes
122
+ * ```
123
+ */
124
+ declare function groupRoutesByNavGroups(routes: RouteDefinition[], groups: NavGroup[]): NavGroupWithRoutes[];
91
125
 
92
- export { type BreadcrumbItem, type MenuGroup, type MenuItem, type NavigationItem, type NavigationSection, type RouteDefinition, type RouteMetadata, defineRoute, findRoute, findRouteByPattern, getPageTitle, getUnauthenticatedRedirect, isActive, redirectToAuth, routesToMenuItems };
126
+ export { type BreadcrumbItem, type MenuGroup, type MenuItem, type NavGroup, type NavGroupWithRoutes, type NavigationItem, type NavigationSection, type RouteDefinition, type RouteMetadata, defineRoute, findRoute, findRouteByPattern, getPageTitle, getUnauthenticatedRedirect, groupRoutesByNavGroups, isActive, redirectToAuth, routesToMenuItems };
@@ -50,12 +50,26 @@ function routesToMenuItems(routes, groupName) {
50
50
  icon: r.metadata.icon
51
51
  }));
52
52
  }
53
+ function groupRoutesByNavGroups(routes, groups) {
54
+ return groups.map((group) => ({
55
+ ...group,
56
+ routes: routes.filter(
57
+ (r) => r.metadata.group === group.id && (r.metadata.show === void 0 || r.metadata.show === true)
58
+ ).sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0))
59
+ })).filter((group) => {
60
+ if (group.dynamic) {
61
+ return group.routes.length > 0;
62
+ }
63
+ return true;
64
+ }).sort((a, b) => a.order - b.order);
65
+ }
53
66
  export {
54
67
  defineRoute,
55
68
  findRoute,
56
69
  findRouteByPattern,
57
70
  getPageTitle,
58
71
  getUnauthenticatedRedirect,
72
+ groupRoutesByNavGroups,
59
73
  isActive,
60
74
  redirectToAuth,
61
75
  routesToMenuItems
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/**\n * Navigation Utilities\n *\n * Common utilities for route definitions and navigation\n */\n\nimport type { RouteDefinition, RouteMetadata, MenuItem } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Definition Helper\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Define a route with metadata\n *\n * IMPORTANT: Next.js automatically handles basePath for <Link> components when\n * basePath is set in next.config.ts. We should NOT add basePath manually here,\n * as it would cause double-prefixing in static builds.\n *\n * @param path - Route path (e.g., '/dashboard', '/admin/users')\n * @param metadata - Route metadata (label, icon, etc.)\n */\nexport function defineRoute(\n path: string,\n metadata: RouteMetadata,\n): RouteDefinition {\n // Always return path as-is. Next.js will handle basePath automatically\n // for <Link> components when basePath is set in next.config.ts\n return {\n path,\n metadata,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Guards\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Get redirect path for unauthenticated users\n */\nexport function getUnauthenticatedRedirect(\n path: string,\n authPath: string = '/auth',\n): string | null {\n if (path.startsWith('/private') || path.startsWith('/admin')) {\n // Return path as-is. Next.js will handle basePath automatically for router.push()\n // when basePath is set in next.config.ts\n return authPath;\n }\n return null;\n}\n\n/**\n * Get redirect path to auth page\n */\nexport function redirectToAuth(\n authPath: string = '/auth',\n): string {\n // Return path as-is. Next.js will handle basePath automatically for router.push()\n // when basePath is set in next.config.ts\n return authPath;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Lookup\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function findRoute(\n routes: RouteDefinition[],\n path: string\n): RouteDefinition | undefined {\n return routes.find((r) => r.path === path);\n}\n\nexport function findRouteByPattern(\n routes: RouteDefinition[],\n path: string\n): RouteDefinition | undefined {\n const exact = findRoute(routes, path);\n if (exact) return exact;\n\n const segments = path.split('/').filter(Boolean);\n for (let i = segments.length; i > 0; i--) {\n const parentPath = '/' + segments.slice(0, i).join('/');\n const parent = findRoute(routes, parentPath);\n if (parent) return parent;\n }\n\n return undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Page Title\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function getPageTitle(\n routes: RouteDefinition[],\n path: string,\n fallback = 'Dashboard'\n): string {\n const route = findRouteByPattern(routes, path);\n return route?.metadata.label || fallback;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Active Route\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a route is active\n *\n * @param current - Current pathname\n * @param target - Target route path to check\n * @param allRoutes - Optional array of all routes to prevent parent paths from being active when child paths are active\n * @returns true if the route is active\n */\nexport function isActive(\n current: string,\n target: string,\n allRoutes?: RouteDefinition[]\n): boolean {\n const matches =\n current === target || (target !== '/' && current.startsWith(target + '/'));\n\n // If allRoutes is provided, check for more specific paths\n if (matches && allRoutes) {\n return !allRoutes.some(\n (otherRoute) =>\n otherRoute.path !== target &&\n otherRoute.path.startsWith(target + '/') &&\n (current === otherRoute.path ||\n current.startsWith(otherRoute.path + '/'))\n );\n }\n\n return matches;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Menu Generation Helper\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Filter and convert routes to menu items\n */\nexport function routesToMenuItems(\n routes: RouteDefinition[],\n groupName: string\n): MenuItem[] {\n return routes\n .filter(\n (r) =>\n r.metadata.group === groupName &&\n r.metadata.icon &&\n (r.metadata.show === undefined || r.metadata.show === true)\n )\n .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0))\n .map((r) => ({\n path: r.path,\n label: r.metadata.label,\n icon: r.metadata.icon!,\n }));\n}\n\n"],"mappings":";AAsBO,SAAS,YACd,MACA,UACiB;AAGjB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AASO,SAAS,2BACd,MACA,WAAmB,SACJ;AACf,MAAI,KAAK,WAAW,UAAU,KAAK,KAAK,WAAW,QAAQ,GAAG;AAG5D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,eACd,WAAmB,SACX;AAGR,SAAO;AACT;AAMO,SAAS,UACd,QACA,MAC6B;AAC7B,SAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC3C;AAEO,SAAS,mBACd,QACA,MAC6B;AAC7B,QAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,MAAI,MAAO,QAAO;AAElB,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,WAAS,IAAI,SAAS,QAAQ,IAAI,GAAG,KAAK;AACxC,UAAM,aAAa,MAAM,SAAS,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACtD,UAAM,SAAS,UAAU,QAAQ,UAAU;AAC3C,QAAI,OAAQ,QAAO;AAAA,EACrB;AAEA,SAAO;AACT;AAMO,SAAS,aACd,QACA,MACA,WAAW,aACH;AACR,QAAM,QAAQ,mBAAmB,QAAQ,IAAI;AAC7C,SAAO,OAAO,SAAS,SAAS;AAClC;AAcO,SAAS,SACd,SACA,QACA,WACS;AACT,QAAM,UACJ,YAAY,UAAW,WAAW,OAAO,QAAQ,WAAW,SAAS,GAAG;AAG1E,MAAI,WAAW,WAAW;AACxB,WAAO,CAAC,UAAU;AAAA,MAChB,CAAC,eACC,WAAW,SAAS,UACpB,WAAW,KAAK,WAAW,SAAS,GAAG,MACtC,YAAY,WAAW,QACtB,QAAQ,WAAW,WAAW,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAEA,SAAO;AACT;AASO,SAAS,kBACd,QACA,WACY;AACZ,SAAO,OACJ;AAAA,IACC,CAAC,MACC,EAAE,SAAS,UAAU,aACrB,EAAE,SAAS,SACV,EAAE,SAAS,SAAS,UAAa,EAAE,SAAS,SAAS;AAAA,EAC1D,EACC,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,SAAS,MAAM,EAAE,SAAS,SAAS,EAAE,EAChE,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE,SAAS;AAAA,IAClB,MAAM,EAAE,SAAS;AAAA,EACnB,EAAE;AACN;","names":[]}
1
+ {"version":3,"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/**\n * Navigation Utilities\n *\n * Common utilities for route definitions and navigation\n */\n\nimport type { RouteDefinition, RouteMetadata, MenuItem, NavGroup, NavGroupWithRoutes } from './types';\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Definition Helper\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Define a route with metadata\n *\n * IMPORTANT: Next.js automatically handles basePath for <Link> components when\n * basePath is set in next.config.ts. We should NOT add basePath manually here,\n * as it would cause double-prefixing in static builds.\n *\n * @param path - Route path (e.g., '/dashboard', '/admin/users')\n * @param metadata - Route metadata (label, icon, etc.)\n */\nexport function defineRoute(\n path: string,\n metadata: RouteMetadata,\n): RouteDefinition {\n // Always return path as-is. Next.js will handle basePath automatically\n // for <Link> components when basePath is set in next.config.ts\n return {\n path,\n metadata,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Guards\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Get redirect path for unauthenticated users\n */\nexport function getUnauthenticatedRedirect(\n path: string,\n authPath: string = '/auth',\n): string | null {\n if (path.startsWith('/private') || path.startsWith('/admin')) {\n // Return path as-is. Next.js will handle basePath automatically for router.push()\n // when basePath is set in next.config.ts\n return authPath;\n }\n return null;\n}\n\n/**\n * Get redirect path to auth page\n */\nexport function redirectToAuth(\n authPath: string = '/auth',\n): string {\n // Return path as-is. Next.js will handle basePath automatically for router.push()\n // when basePath is set in next.config.ts\n return authPath;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Route Lookup\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function findRoute(\n routes: RouteDefinition[],\n path: string\n): RouteDefinition | undefined {\n return routes.find((r) => r.path === path);\n}\n\nexport function findRouteByPattern(\n routes: RouteDefinition[],\n path: string\n): RouteDefinition | undefined {\n const exact = findRoute(routes, path);\n if (exact) return exact;\n\n const segments = path.split('/').filter(Boolean);\n for (let i = segments.length; i > 0; i--) {\n const parentPath = '/' + segments.slice(0, i).join('/');\n const parent = findRoute(routes, parentPath);\n if (parent) return parent;\n }\n\n return undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Page Title\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function getPageTitle(\n routes: RouteDefinition[],\n path: string,\n fallback = 'Dashboard'\n): string {\n const route = findRouteByPattern(routes, path);\n return route?.metadata.label || fallback;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Active Route\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Check if a route is active\n *\n * @param current - Current pathname\n * @param target - Target route path to check\n * @param allRoutes - Optional array of all routes to prevent parent paths from being active when child paths are active\n * @returns true if the route is active\n */\nexport function isActive(\n current: string,\n target: string,\n allRoutes?: RouteDefinition[]\n): boolean {\n const matches =\n current === target || (target !== '/' && current.startsWith(target + '/'));\n\n // If allRoutes is provided, check for more specific paths\n if (matches && allRoutes) {\n return !allRoutes.some(\n (otherRoute) =>\n otherRoute.path !== target &&\n otherRoute.path.startsWith(target + '/') &&\n (current === otherRoute.path ||\n current.startsWith(otherRoute.path + '/'))\n );\n }\n\n return matches;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Menu Generation Helper\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Filter and convert routes to menu items\n */\nexport function routesToMenuItems(\n routes: RouteDefinition[],\n groupName: string\n): MenuItem[] {\n return routes\n .filter(\n (r) =>\n r.metadata.group === groupName &&\n r.metadata.icon &&\n (r.metadata.show === undefined || r.metadata.show === true)\n )\n .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0))\n .map((r) => ({\n path: r.path,\n label: r.metadata.label,\n icon: r.metadata.icon!,\n }));\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Navigation Groups Helper\n// ─────────────────────────────────────────────────────────────────────────\n\n/**\n * Group routes by navigation groups\n *\n * @param routes - Array of route definitions\n * @param groups - Array of navigation group definitions\n * @returns Array of groups with their routes, filtered by dynamic flag\n *\n * @example\n * ```ts\n * const navGroups = [\n * { id: 'dashboard', label: 'Dashboard', order: 1 },\n * { id: 'extensions', label: 'Extensions', order: 2, dynamic: true },\n * ];\n *\n * const groupedRoutes = groupRoutesByNavGroups(allRoutes, navGroups);\n * // Returns groups with routes, hiding dynamic groups with no routes\n * ```\n */\nexport function groupRoutesByNavGroups(\n routes: RouteDefinition[],\n groups: NavGroup[]\n): NavGroupWithRoutes[] {\n return groups\n .map((group) => ({\n ...group,\n routes: routes\n .filter(\n (r) =>\n r.metadata.group === group.id &&\n (r.metadata.show === undefined || r.metadata.show === true)\n )\n .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0)),\n }))\n .filter((group) => {\n // Dynamic groups are only shown if they have routes\n if (group.dynamic) {\n return group.routes.length > 0;\n }\n // Non-dynamic groups are always shown\n return true;\n })\n .sort((a, b) => a.order - b.order);\n}\n\n"],"mappings":";AAsBO,SAAS,YACd,MACA,UACiB;AAGjB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AASO,SAAS,2BACd,MACA,WAAmB,SACJ;AACf,MAAI,KAAK,WAAW,UAAU,KAAK,KAAK,WAAW,QAAQ,GAAG;AAG5D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKO,SAAS,eACd,WAAmB,SACX;AAGR,SAAO;AACT;AAMO,SAAS,UACd,QACA,MAC6B;AAC7B,SAAO,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI;AAC3C;AAEO,SAAS,mBACd,QACA,MAC6B;AAC7B,QAAM,QAAQ,UAAU,QAAQ,IAAI;AACpC,MAAI,MAAO,QAAO;AAElB,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,WAAS,IAAI,SAAS,QAAQ,IAAI,GAAG,KAAK;AACxC,UAAM,aAAa,MAAM,SAAS,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACtD,UAAM,SAAS,UAAU,QAAQ,UAAU;AAC3C,QAAI,OAAQ,QAAO;AAAA,EACrB;AAEA,SAAO;AACT;AAMO,SAAS,aACd,QACA,MACA,WAAW,aACH;AACR,QAAM,QAAQ,mBAAmB,QAAQ,IAAI;AAC7C,SAAO,OAAO,SAAS,SAAS;AAClC;AAcO,SAAS,SACd,SACA,QACA,WACS;AACT,QAAM,UACJ,YAAY,UAAW,WAAW,OAAO,QAAQ,WAAW,SAAS,GAAG;AAG1E,MAAI,WAAW,WAAW;AACxB,WAAO,CAAC,UAAU;AAAA,MAChB,CAAC,eACC,WAAW,SAAS,UACpB,WAAW,KAAK,WAAW,SAAS,GAAG,MACtC,YAAY,WAAW,QACtB,QAAQ,WAAW,WAAW,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAEA,SAAO;AACT;AASO,SAAS,kBACd,QACA,WACY;AACZ,SAAO,OACJ;AAAA,IACC,CAAC,MACC,EAAE,SAAS,UAAU,aACrB,EAAE,SAAS,SACV,EAAE,SAAS,SAAS,UAAa,EAAE,SAAS,SAAS;AAAA,EAC1D,EACC,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,SAAS,MAAM,EAAE,SAAS,SAAS,EAAE,EAChE,IAAI,CAAC,OAAO;AAAA,IACX,MAAM,EAAE;AAAA,IACR,OAAO,EAAE,SAAS;AAAA,IAClB,MAAM,EAAE,SAAS;AAAA,EACnB,EAAE;AACN;AAwBO,SAAS,uBACd,QACA,QACsB;AACtB,SAAO,OACJ,IAAI,CAAC,WAAW;AAAA,IACf,GAAG;AAAA,IACH,QAAQ,OACL;AAAA,MACC,CAAC,MACC,EAAE,SAAS,UAAU,MAAM,OAC1B,EAAE,SAAS,SAAS,UAAa,EAAE,SAAS,SAAS;AAAA,IAC1D,EACC,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,SAAS,MAAM,EAAE,SAAS,SAAS,EAAE;AAAA,EACrE,EAAE,EACD,OAAO,CAAC,UAAU;AAEjB,QAAI,MAAM,SAAS;AACjB,aAAO,MAAM,OAAO,SAAS;AAAA,IAC/B;AAEA,WAAO;AAAA,EACT,CAAC,EACA,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACrC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/nextjs",
3
- "version": "2.1.107",
3
+ "version": "2.1.109",
4
4
  "description": "Next.js server utilities: sitemap, health, OG images, contact forms, navigation, config",
5
5
  "keywords": [
6
6
  "nextjs",
@@ -133,9 +133,9 @@
133
133
  "web-push": "^3.6.7"
134
134
  },
135
135
  "devDependencies": {
136
- "@djangocfg/imgai": "^2.1.107",
137
- "@djangocfg/layouts": "^2.1.107",
138
- "@djangocfg/typescript-config": "^2.1.107",
136
+ "@djangocfg/imgai": "^2.1.109",
137
+ "@djangocfg/layouts": "^2.1.109",
138
+ "@djangocfg/typescript-config": "^2.1.109",
139
139
  "@types/node": "^24.7.2",
140
140
  "@types/react": "19.2.2",
141
141
  "@types/react-dom": "19.2.1",
@@ -18,6 +18,8 @@ export interface RouteMetadata {
18
18
  group?: string;
19
19
  order?: number;
20
20
  show?: boolean;
21
+ // Extension mapping (for dynamic filtering based on API response)
22
+ extensionName?: string;
21
23
  // SEO
22
24
  priority?: number; // Sitemap priority 0.0-1.0
23
25
  changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
@@ -29,6 +31,26 @@ export interface RouteDefinition {
29
31
  metadata: RouteMetadata;
30
32
  }
31
33
 
34
+ // ─────────────────────────────────────────────────────────────────────────
35
+ // Navigation Group Types (for horizontal tabs with labels)
36
+ // ─────────────────────────────────────────────────────────────────────────
37
+
38
+ export interface NavGroup {
39
+ /** Unique group identifier (matches RouteMetadata.group) */
40
+ id: string;
41
+ /** Display label for the group */
42
+ label: string;
43
+ /** Sort order */
44
+ order: number;
45
+ /** If true, group is only shown when it has items (e.g., extensions) */
46
+ dynamic?: boolean;
47
+ }
48
+
49
+ export interface NavGroupWithRoutes extends NavGroup {
50
+ /** Routes in this group (populated at runtime) */
51
+ routes: RouteDefinition[];
52
+ }
53
+
32
54
  // ─────────────────────────────────────────────────────────────────────────
33
55
  // Menu Types
34
56
  // ─────────────────────────────────────────────────────────────────────────
@@ -4,7 +4,7 @@
4
4
  * Common utilities for route definitions and navigation
5
5
  */
6
6
 
7
- import type { RouteDefinition, RouteMetadata, MenuItem } from './types';
7
+ import type { RouteDefinition, RouteMetadata, MenuItem, NavGroup, NavGroupWithRoutes } from './types';
8
8
 
9
9
  // ─────────────────────────────────────────────────────────────────────────
10
10
  // Route Definition Helper
@@ -163,3 +163,51 @@ export function routesToMenuItems(
163
163
  }));
164
164
  }
165
165
 
166
+ // ─────────────────────────────────────────────────────────────────────────
167
+ // Navigation Groups Helper
168
+ // ─────────────────────────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * Group routes by navigation groups
172
+ *
173
+ * @param routes - Array of route definitions
174
+ * @param groups - Array of navigation group definitions
175
+ * @returns Array of groups with their routes, filtered by dynamic flag
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const navGroups = [
180
+ * { id: 'dashboard', label: 'Dashboard', order: 1 },
181
+ * { id: 'extensions', label: 'Extensions', order: 2, dynamic: true },
182
+ * ];
183
+ *
184
+ * const groupedRoutes = groupRoutesByNavGroups(allRoutes, navGroups);
185
+ * // Returns groups with routes, hiding dynamic groups with no routes
186
+ * ```
187
+ */
188
+ export function groupRoutesByNavGroups(
189
+ routes: RouteDefinition[],
190
+ groups: NavGroup[]
191
+ ): NavGroupWithRoutes[] {
192
+ return groups
193
+ .map((group) => ({
194
+ ...group,
195
+ routes: routes
196
+ .filter(
197
+ (r) =>
198
+ r.metadata.group === group.id &&
199
+ (r.metadata.show === undefined || r.metadata.show === true)
200
+ )
201
+ .sort((a, b) => (a.metadata.order || 0) - (b.metadata.order || 0)),
202
+ }))
203
+ .filter((group) => {
204
+ // Dynamic groups are only shown if they have routes
205
+ if (group.dynamic) {
206
+ return group.routes.length > 0;
207
+ }
208
+ // Non-dynamic groups are always shown
209
+ return true;
210
+ })
211
+ .sort((a, b) => a.order - b.order);
212
+ }
213
+