@eventcatalog/core 2.30.7 → 2.31.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 (60) hide show
  1. package/README.md +3 -2
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +9 -3
  5. package/dist/analytics/log-build.d.cts +4 -1
  6. package/dist/analytics/log-build.d.ts +4 -1
  7. package/dist/analytics/log-build.js +3 -3
  8. package/dist/{chunk-SUJLBNZK.js → chunk-4S3UNXH2.js} +1 -1
  9. package/dist/{chunk-HINNLTBH.js → chunk-D7LV5JLL.js} +9 -3
  10. package/dist/{chunk-EFSBN3ZZ.js → chunk-I6OFOESY.js} +1 -1
  11. package/dist/{chunk-XMDPVKIJ.js → chunk-NJGR7XUU.js} +44 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/eventcatalog.cjs +74 -14
  15. package/dist/eventcatalog.config.d.cts +28 -0
  16. package/dist/eventcatalog.config.d.ts +28 -0
  17. package/dist/eventcatalog.js +29 -16
  18. package/dist/features.cjs +46 -2
  19. package/dist/features.d.cts +2 -1
  20. package/dist/features.d.ts +2 -1
  21. package/dist/features.js +5 -3
  22. package/eventcatalog/public/images/custom-docs-placeholder.png +0 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +0 -2
  24. package/eventcatalog/src/components/MDX/Steps/Step.astro +1 -1
  25. package/eventcatalog/src/components/MDX/Steps/Steps.astro +15 -0
  26. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +75 -0
  27. package/eventcatalog/src/components/SideNav/CustomDocsNav/CustomDocsNavWrapper.tsx +11 -0
  28. package/eventcatalog/src/components/SideNav/CustomDocsNav/components/NestedItem.tsx +183 -0
  29. package/eventcatalog/src/components/SideNav/CustomDocsNav/components/NoResultsFound.tsx +21 -0
  30. package/eventcatalog/src/components/SideNav/CustomDocsNav/index.tsx +250 -0
  31. package/eventcatalog/src/components/SideNav/CustomDocsNav/types.ts +29 -0
  32. package/eventcatalog/src/components/SideNav/CustomDocsNav.astro +9 -0
  33. package/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +2 -2
  34. package/eventcatalog/src/content.config.ts +15 -24
  35. package/eventcatalog/src/enterprise/collections/custom-pages.ts +19 -0
  36. package/eventcatalog/src/enterprise/custom-documentation/collection.ts +16 -0
  37. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper.tsx +11 -0
  38. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx +183 -0
  39. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx +21 -0
  40. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +250 -0
  41. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/types.ts +29 -0
  42. package/eventcatalog/src/enterprise/custom-documentation/pages/index.astro +389 -0
  43. package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +118 -0
  44. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +58 -9
  45. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/index.astro +23 -3
  46. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +13 -3
  47. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +134 -17
  48. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/index.astro +21 -3
  49. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -1
  50. package/eventcatalog/src/pages/docs/[type]/[id]/language.astro +1 -1
  51. package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +260 -0
  52. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +5 -3
  53. package/eventcatalog/src/pages/docs/users/[id]/index.astro +9 -4
  54. package/eventcatalog/src/pages/pro/index.astro +272 -0
  55. package/eventcatalog/src/shared-collections.ts +25 -0
  56. package/eventcatalog/src/types/index.ts +1 -1
  57. package/eventcatalog/src/utils/eventcatalog-config/catalog.ts +12 -1
  58. package/eventcatalog/src/utils/feature.ts +5 -0
  59. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  60. package/package.json +1 -1
@@ -38,8 +38,6 @@ function classNames(...classes: any) {
38
38
  export default function UserNode({ data, sourcePosition, targetPosition }: any) {
39
39
  const { mode, step, custom: customProps } = data as Data;
40
40
 
41
- console.log('step', customProps);
42
-
43
41
  const {
44
42
  color = 'blue',
45
43
  title = 'Custom',
@@ -2,7 +2,7 @@
2
2
  const { title } = Astro.props;
3
3
  ---
4
4
 
5
- <li class="mb-8 ml-6 w-full xl:max-w-[50%] step">
5
+ <li class="mb-8 ml-6 w-full xl:max-w-[90%] step">
6
6
  <div class="flex items-center mb-4">
7
7
  <span class="step-number absolute flex items-center justify-center w-6 h-6 bg-primary/80 rounded-md -left-3 text-white">
8
8
  <div></div>
@@ -25,3 +25,18 @@ const data = splitByLi(html);
25
25
  {data.map((item: any) => <Fragment set:html={item} />)}
26
26
  </ol>
27
27
  </div>
28
+
29
+ <script>
30
+ function applyStepNumbers() {
31
+ document.querySelectorAll('li[data-step]').forEach((li) => {
32
+ const stepNumber = li.getAttribute('data-step');
33
+ const stepSpan = li.querySelector('.step-number div');
34
+ if (stepSpan) {
35
+ stepSpan.textContent = stepNumber;
36
+ }
37
+ });
38
+ }
39
+
40
+ document.addEventListener('DOMContentLoaded', applyStepNumbers);
41
+ document.addEventListener('astro:page-load', applyStepNumbers);
42
+ </script>
@@ -0,0 +1,75 @@
1
+ ---
2
+ import OwnersList from '@components/Lists/OwnersList';
3
+ import VersionList from '@components/Lists/VersionList.astro';
4
+ import { buildUrl } from '@utils/url-builder';
5
+ import { getOwner } from '@utils/collections/owners';
6
+ import type { CollectionEntry } from 'astro:content';
7
+ import { ScrollText, Workflow, RssIcon } from 'lucide-react';
8
+ import config from '@config';
9
+
10
+ interface Props {
11
+ flow: CollectionEntry<'flows'>;
12
+ }
13
+
14
+ const { flow } = Astro.props;
15
+
16
+ const ownersRaw = flow.data?.owners || [];
17
+ const owners = await Promise.all<ReturnType<typeof getOwner>>(ownersRaw.map(getOwner));
18
+ const filteredOwners = owners.filter((o) => o !== undefined);
19
+
20
+ const ownersList = filteredOwners.map((o) => ({
21
+ label: o.data.name,
22
+ type: o.collection,
23
+ badge: o.collection === 'users' ? o.data.role : 'Team',
24
+ avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '',
25
+ href: buildUrl(`/docs/${o.collection}/${o.data.id}`),
26
+ }));
27
+
28
+ const isRSSEnabled = config.rss?.enabled;
29
+ ---
30
+
31
+ <aside class="sticky top-28 left-0 h-full overflow-y-auto pr-6 py-4">
32
+ <div id="sidebar-cta-portal" class="">
33
+ {flow.data.versions && <VersionList versions={flow.data.versions} collectionItem={flow} />}
34
+
35
+ <OwnersList
36
+ title={`Flow owners (${ownersList.length})`}
37
+ owners={ownersList}
38
+ emptyMessage={`This flow does not have any documented owners.`}
39
+ client:load
40
+ />
41
+
42
+ {
43
+ isRSSEnabled && (
44
+ <div class="mx-auto pb-4 w-full max-w-lg divide-y divide-white/5 rounded-xl bg-white/5 border-b border-gray-100 mb-4">
45
+ <span class="text-sm text-black group-data-[hover]:text-black/80 capitalize">Flow RSS Feed</span>
46
+ <ul role="list" class="space-y-2 mt-2">
47
+ <li class="has-tooltip rounded-md text-gray-600 group px-1 w-full hover:bg-gradient-to-l hover:from-purple-500 hover:to-purple-700 hover:text-white hover:font-normal ">
48
+ <a class={`flex items-center space-x-2`} target="_blank" href={buildUrl(`/rss/flows/rss.xml`)}>
49
+ <RssIcon className="h-4 w-4 text-gray-800 group-hover:text-white" strokeWidth={1} />
50
+ <span class="font-light text-sm truncate">RSS</span>
51
+ </a>
52
+ </li>
53
+ </ul>
54
+ </div>
55
+ )
56
+ }
57
+
58
+ <div class="space-y-2">
59
+ <a
60
+ href={buildUrl(`/visualiser/${flow.collection}/${flow.data.id}/${flow.data.version}`)}
61
+ class="flex items-center justify-center space-x-2 text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-primary"
62
+ >
63
+ <Workflow strokeWidth={2} size={16} />
64
+ <span class="block">View in visualiser</span>
65
+ </a>
66
+ <a
67
+ href={buildUrl(`/docs/${flow.collection}/${flow.data.id}/${flow.data.latestVersion}/changelog`)}
68
+ class="flex items-center space-x-2 justify-center text-center rounded-md w-full bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-100/60 hover:text-primary"
69
+ >
70
+ <ScrollText strokeWidth={2} size={16} />
71
+ <span class="block">Read changelog</span>
72
+ </a>
73
+ </div>
74
+ </div>
75
+ </aside>
@@ -0,0 +1,11 @@
1
+ import CustomDocsNav from './';
2
+ import type { SidebarSection } from './types';
3
+
4
+ interface CustomDocsNavWrapperProps {
5
+ sidebarItems: SidebarSection[];
6
+ currentPath: string;
7
+ }
8
+
9
+ export default function CustomDocsNavWrapper(props: CustomDocsNavWrapperProps) {
10
+ return <CustomDocsNav {...props} />;
11
+ }
@@ -0,0 +1,183 @@
1
+ import React from 'react';
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import type { SidebarItem } from '../types';
4
+
5
+ interface NestedItemProps {
6
+ item: SidebarItem;
7
+ currentPath: string;
8
+ parentId: string;
9
+ itemIndex: number;
10
+ collapsedGroups: { [key: string]: boolean };
11
+ toggleGroupCollapse: (group: string) => void;
12
+ }
13
+
14
+ const NestedItem: React.FC<NestedItemProps> = ({
15
+ item,
16
+ currentPath,
17
+ parentId,
18
+ itemIndex,
19
+ collapsedGroups,
20
+ toggleGroupCollapse,
21
+ }) => {
22
+ const hasNestedItems = item.items && item.items.length > 0;
23
+ const itemId = `${parentId}-${itemIndex}`;
24
+
25
+ if (hasNestedItems && item.items) {
26
+ return (
27
+ <div className="py-1">
28
+ <div className="flex items-center">
29
+ <button
30
+ className="p-1 hover:bg-gray-100 rounded-md"
31
+ onClick={(e) => {
32
+ e.stopPropagation();
33
+ toggleGroupCollapse(`nested-${itemId}`);
34
+ }}
35
+ >
36
+ <div className={`transition-transform duration-150 ${collapsedGroups[`nested-${itemId}`] ? '' : 'rotate-180'}`}>
37
+ <svg
38
+ className="h-3 w-3 text-gray-500"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ width="16"
41
+ height="16"
42
+ viewBox="0 0 24 24"
43
+ fill="none"
44
+ stroke="currentColor"
45
+ strokeWidth="2"
46
+ strokeLinecap="round"
47
+ strokeLinejoin="round"
48
+ >
49
+ <polyline points="6 9 12 15 18 9" />
50
+ </svg>
51
+ </div>
52
+ </button>
53
+ <button
54
+ className="flex-grow flex items-center px-2 py-1 text-xs font-medium text-gray-700 rounded-md hover:bg-purple-50"
55
+ onClick={(e) => {
56
+ e.stopPropagation();
57
+ toggleGroupCollapse(`nested-${itemId}`);
58
+ }}
59
+ >
60
+ <span className="truncate">{item.label}</span>
61
+ </button>
62
+ </div>
63
+
64
+ <div
65
+ className={`overflow-hidden transition-[height] duration-150 ease-out ${
66
+ collapsedGroups[`nested-${itemId}`] ? 'h-0' : 'h-auto'
67
+ }`}
68
+ >
69
+ <div className="space-y-0.5 border-gray-200/80 border-l pl-4 ml-[9px] mt-1">
70
+ {item.items.map((nestedItem: SidebarItem, nestedIndex: number) => {
71
+ if (nestedItem.items && nestedItem.items.length > 0) {
72
+ // Recursively render deeper nested items
73
+ const nestedItemId = `${itemId}-${nestedIndex}`;
74
+ return (
75
+ <div className="py-1" key={`nested-${nestedItemId}`}>
76
+ <div className="flex items-center">
77
+ <button
78
+ className="p-1 hover:bg-gray-100 rounded-md"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ toggleGroupCollapse(`nested-${nestedItemId}`);
82
+ }}
83
+ >
84
+ <div
85
+ className={`transition-transform duration-150 ${collapsedGroups[`nested-${nestedItemId}`] ? '' : 'rotate-180'}`}
86
+ >
87
+ <svg
88
+ className="h-3 w-3 text-gray-500"
89
+ xmlns="http://www.w3.org/2000/svg"
90
+ width="16"
91
+ height="16"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ >
99
+ <polyline points="6 9 12 15 18 9" />
100
+ </svg>
101
+ </div>
102
+ </button>
103
+ <button
104
+ className="flex-grow flex items-center px-2 py-1 text-xs font-medium text-gray-700 rounded-md hover:bg-purple-50"
105
+ onClick={(e) => {
106
+ e.stopPropagation();
107
+ toggleGroupCollapse(`nested-${nestedItemId}`);
108
+ }}
109
+ >
110
+ <span className="truncate">{nestedItem.label}</span>
111
+ </button>
112
+ </div>
113
+
114
+ <div
115
+ className={`overflow-hidden transition-[height] duration-150 ease-out ${
116
+ collapsedGroups[`nested-${nestedItemId}`] ? 'h-0' : 'h-auto'
117
+ }`}
118
+ >
119
+ <div className="space-y-0.5 border-gray-200/80 border-l pl-4 ml-[9px] mt-1">
120
+ {nestedItem.items.map((deepNestedItem: SidebarItem, deepIndex: number) => {
121
+ const deepNestedItemPath = deepNestedItem.slug ? buildUrl(`/docs/custom/${deepNestedItem.slug}`) : '#';
122
+ const isDeepActive =
123
+ currentPath === deepNestedItemPath || currentPath.endsWith(`/${deepNestedItem.slug}`);
124
+
125
+ return (
126
+ <a
127
+ key={`deep-${nestedItemId}-${deepIndex}`}
128
+ href={deepNestedItemPath}
129
+ className={`flex items-center px-2 py-1.5 text-xs ${isDeepActive ? 'bg-purple-100 text-purple-900 font-medium' : 'text-gray-600 hover:bg-purple-100'} rounded-md`}
130
+ data-active={isDeepActive}
131
+ >
132
+ <span className="truncate">{deepNestedItem.label}</span>
133
+ </a>
134
+ );
135
+ })}
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ const nestedItemPath = nestedItem.slug ? buildUrl(`/docs/custom/${nestedItem.slug}`) : '#';
143
+ const isActive = currentPath === nestedItemPath || currentPath.endsWith(`/${nestedItem.slug}`);
144
+
145
+ return (
146
+ <a
147
+ key={`nested-link-${itemId}-${nestedIndex}`}
148
+ href={nestedItemPath}
149
+ className={`flex items-center px-2 py-1.5 text-xs ${isActive ? 'bg-purple-100 text-purple-900 font-medium' : 'text-gray-600 hover:bg-purple-100'} rounded-md`}
150
+ data-active={isActive}
151
+ >
152
+ <span className="truncate">{nestedItem.label}</span>
153
+ </a>
154
+ );
155
+ })}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ const itemPath = item.slug ? buildUrl(`/docs/custom/${item.slug}`) : '#';
163
+ const isActive = currentPath === itemPath || currentPath.endsWith(`/${item.slug}`);
164
+
165
+ return (
166
+ <a
167
+ href={itemPath}
168
+ className={`flex items-center px-2 py-1.5 text-xs ${isActive ? 'bg-purple-100 text-purple-900 font-medium' : 'text-gray-600 hover:bg-purple-100'} rounded-md`}
169
+ data-active={isActive}
170
+ >
171
+ <span className="truncate">{item.label}</span>
172
+ {item.badge && item?.badge?.text && (
173
+ <span
174
+ className={`text-${item.badge.color || 'purple'}-600 ml-2 text-[10px] font-medium bg-${item.badge.color || 'purple'}-50 px-2 py-0.5 rounded uppercase`}
175
+ >
176
+ {item.badge.text}
177
+ </span>
178
+ )}
179
+ </a>
180
+ );
181
+ };
182
+
183
+ export default React.memo(NestedItem);
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+
3
+ interface NoResultsFoundProps {
4
+ searchTerm: string;
5
+ }
6
+
7
+ const NoResultsFound: React.FC<NoResultsFoundProps> = ({ searchTerm }) => (
8
+ <div className="px-4 py-6 text-center">
9
+ <div className="text-gray-400 text-sm mb-2">No results found for "{searchTerm}"</div>
10
+ <div className="text-gray-400 text-xs">
11
+ Try:
12
+ <ul className="mt-2 space-y-1 text-left list-disc pl-4">
13
+ <li>Checking for typos</li>
14
+ <li>Using fewer keywords</li>
15
+ <li>Using more general terms</li>
16
+ </ul>
17
+ </div>
18
+ </div>
19
+ );
20
+
21
+ export default React.memo(NoResultsFound);
@@ -0,0 +1,250 @@
1
+ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import type { CustomDocsNavProps, SidebarSection, SidebarItem } from './types';
4
+ import NestedItem from './components/NestedItem';
5
+ import NoResultsFound from './components/NoResultsFound';
6
+
7
+ const STORAGE_KEY = 'EventCatalog:customDocsSidebarCollapsedGroups';
8
+ const DEBOUNCE_DELAY = 300; // 300ms debounce delay
9
+
10
+ const CustomDocsNav: React.FC<CustomDocsNavProps> = ({ sidebarItems, currentPath }) => {
11
+ const navRef = useRef<HTMLElement>(null);
12
+ const [searchTerm, setSearchTerm] = useState('');
13
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
14
+ const [isInitialized, setIsInitialized] = useState(false);
15
+ const [collapsedGroups, setCollapsedGroups] = useState<{ [key: string]: boolean }>(() => {
16
+ if (typeof window !== 'undefined') {
17
+ const saved = window.localStorage.getItem(STORAGE_KEY);
18
+ setIsInitialized(true);
19
+ return saved ? JSON.parse(saved) : {};
20
+ }
21
+ return {};
22
+ });
23
+
24
+ // Set up debounced search
25
+ useEffect(() => {
26
+ const timer = setTimeout(() => {
27
+ setDebouncedSearchTerm(searchTerm.toLowerCase());
28
+ }, DEBOUNCE_DELAY);
29
+
30
+ return () => clearTimeout(timer);
31
+ }, [searchTerm]);
32
+
33
+ // Filter sidebar items based on search term
34
+ const filteredSidebarItems = useMemo(() => {
35
+ if (!debouncedSearchTerm) return sidebarItems;
36
+
37
+ const matchesSearchTerm = (text: string) => text.toLowerCase().includes(debouncedSearchTerm);
38
+
39
+ // Helper function to check if an item or any of its nested items match the search term
40
+ const itemContainsSearchTerm = (item: SidebarItem): boolean => {
41
+ if (matchesSearchTerm(item.label)) return true;
42
+
43
+ if (item.items && item.items.length > 0) {
44
+ return item.items.some(itemContainsSearchTerm);
45
+ }
46
+
47
+ return false;
48
+ };
49
+
50
+ return sidebarItems
51
+ .map((section) => {
52
+ if (!section.items) {
53
+ return matchesSearchTerm(section.label) ? section : null;
54
+ }
55
+
56
+ const filteredItems = section.items.filter(itemContainsSearchTerm);
57
+
58
+ if (filteredItems.length > 0 || matchesSearchTerm(section.label)) {
59
+ return {
60
+ ...section,
61
+ items: filteredItems,
62
+ };
63
+ }
64
+
65
+ return null;
66
+ })
67
+ .filter(Boolean) as SidebarSection[];
68
+ }, [sidebarItems, debouncedSearchTerm]);
69
+
70
+ // Auto-expand groups when searching
71
+ useEffect(() => {
72
+ if (debouncedSearchTerm) {
73
+ // Expand all groups when searching
74
+ const newCollapsedState = { ...collapsedGroups };
75
+ Object.keys(newCollapsedState).forEach((key) => {
76
+ newCollapsedState[key] = false;
77
+ });
78
+ setCollapsedGroups(newCollapsedState);
79
+ }
80
+ }, [debouncedSearchTerm]);
81
+
82
+ // Store collapsed groups in local storage
83
+ useEffect(() => {
84
+ if (typeof window !== 'undefined' && isInitialized) {
85
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedGroups));
86
+ }
87
+ }, [collapsedGroups, isInitialized]);
88
+
89
+ // Initialize collapsed state from section config
90
+ useEffect(() => {
91
+ if (isInitialized && sidebarItems && sidebarItems.length > 0) {
92
+ const initialState = { ...collapsedGroups };
93
+
94
+ sidebarItems.forEach((section, index) => {
95
+ const sectionKey = `section-${index}`;
96
+ if (section.collapsed !== undefined && initialState[sectionKey] === undefined) {
97
+ initialState[sectionKey] = section.collapsed;
98
+ }
99
+ });
100
+
101
+ setCollapsedGroups(initialState);
102
+ }
103
+ }, [sidebarItems, isInitialized]);
104
+
105
+ // If we find a data-active element, scroll to it on mount
106
+ useEffect(() => {
107
+ const activeElement = document.querySelector('[data-active="true"]');
108
+ if (activeElement) {
109
+ // Add y offset to the scroll position
110
+ setTimeout(() => {
111
+ activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
112
+ }, 300);
113
+ }
114
+ }, []);
115
+
116
+ const toggleGroupCollapse = useCallback((group: string) => {
117
+ setCollapsedGroups((prev) => ({
118
+ ...prev,
119
+ [group]: !prev[group],
120
+ }));
121
+ }, []);
122
+
123
+ const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
124
+ setSearchTerm(e.target.value);
125
+ }, []);
126
+
127
+ if (!isInitialized) return null;
128
+
129
+ const hasNoResults = debouncedSearchTerm && filteredSidebarItems.length === 0;
130
+
131
+ return (
132
+ <nav ref={navRef} className="h-full text-gray-800 pt-2">
133
+ <div className="mb-2 px-4">
134
+ <input
135
+ type="text"
136
+ value={searchTerm}
137
+ onChange={handleSearchChange}
138
+ placeholder="Quick search..."
139
+ className="w-full p-2 text-sm rounded-md border border-gray-200 h-[30px]"
140
+ />
141
+ </div>
142
+
143
+ <div className="space-y-2 divide-y divide-gray-100/40">
144
+ {hasNoResults ? (
145
+ <NoResultsFound searchTerm={debouncedSearchTerm} />
146
+ ) : (
147
+ filteredSidebarItems.map((section: SidebarSection, index: number) => (
148
+ <div className="pt-2 pb-2 px-4" key={`section-${index}`}>
149
+ <div className="space-y-0" data-section={`section-${index}`}>
150
+ {section.items ? (
151
+ <div className="flex items-center">
152
+ <button
153
+ className="p-1 hover:bg-gray-100 rounded-md"
154
+ onClick={(e) => {
155
+ e.stopPropagation();
156
+ toggleGroupCollapse(`section-${index}`);
157
+ }}
158
+ >
159
+ <div
160
+ className={`transition-transform duration-150 ${collapsedGroups[`section-${index}`] ? '' : 'rotate-180'}`}
161
+ >
162
+ <svg
163
+ className="h-3 w-3 text-gray-500"
164
+ xmlns="http://www.w3.org/2000/svg"
165
+ width="16"
166
+ height="16"
167
+ viewBox="0 0 24 24"
168
+ fill="none"
169
+ stroke="currentColor"
170
+ strokeWidth="2"
171
+ strokeLinecap="round"
172
+ strokeLinejoin="round"
173
+ >
174
+ <polyline points="6 9 12 15 18 9" />
175
+ </svg>
176
+ </div>
177
+ </button>
178
+ <button
179
+ className="flex-grow flex items-center justify-between px-2 py-0.5 text-xs font-bold rounded-md hover:bg-purple-50"
180
+ onClick={(e) => {
181
+ e.stopPropagation();
182
+ toggleGroupCollapse(`section-${index}`);
183
+ }}
184
+ >
185
+ <span className="truncate">{section.label}</span>
186
+ {section.badge && section?.badge?.text && (
187
+ <span
188
+ className={`text-${section.badge.color || 'purple'}-600 ml-2 text-[10px] font-medium bg-${section.badge.color || 'purple'}-50 px-2 py-0.5 rounded uppercase`}
189
+ >
190
+ {section.badge.text}
191
+ </span>
192
+ )}
193
+ </button>
194
+ </div>
195
+ ) : (
196
+ <div className="flex items-center">
197
+ <span className="flex-grow flex items-center justify-between px-2 py-0.5 text-xs font-bold rounded-md">
198
+ <span className="truncate">{section.label}</span>
199
+ <span className="text-purple-600 ml-2 text-[10px] font-medium bg-purple-50 px-2 py-0.5 rounded uppercase">
200
+ Section
201
+ </span>
202
+ </span>
203
+ </div>
204
+ )}
205
+
206
+ {section.items && (
207
+ <div
208
+ className={`overflow-hidden transition-[height] duration-150 ease-out ${
209
+ collapsedGroups[`section-${index}`] ? 'h-0' : 'h-auto'
210
+ }`}
211
+ >
212
+ <div className="space-y-0.5 border-gray-200/80 border-l pl-4 ml-[9px] mt-1">
213
+ {section.items.map((item: SidebarItem, itemIndex: number) => (
214
+ <NestedItem
215
+ key={`item-${index}-${itemIndex}`}
216
+ item={item}
217
+ currentPath={currentPath}
218
+ parentId={`${index}`}
219
+ itemIndex={itemIndex}
220
+ collapsedGroups={collapsedGroups}
221
+ toggleGroupCollapse={toggleGroupCollapse}
222
+ />
223
+ ))}
224
+ </div>
225
+ </div>
226
+ )}
227
+
228
+ {section.slug && !section.items && (
229
+ <a
230
+ href={buildUrl(`/docs/custom/${section.slug}`)}
231
+ className={`flex items-center px-2 py-1.5 text-xs ${
232
+ currentPath.endsWith(`/${section.slug}`)
233
+ ? 'bg-purple-100 text-purple-900 font-medium'
234
+ : 'text-gray-600 hover:bg-purple-100'
235
+ } rounded-md ml-6`}
236
+ data-active={currentPath.endsWith(`/${section.slug}`)}
237
+ >
238
+ <span className="truncate">{section.label}</span>
239
+ </a>
240
+ )}
241
+ </div>
242
+ </div>
243
+ ))
244
+ )}
245
+ </div>
246
+ </nav>
247
+ );
248
+ };
249
+
250
+ export default React.memo(CustomDocsNav);
@@ -0,0 +1,29 @@
1
+ export interface SidebarItem {
2
+ label: string;
3
+ slug?: string;
4
+ items?: SidebarItem[];
5
+ badge?: {
6
+ text: string;
7
+ color: string;
8
+ };
9
+ collapsed?: boolean;
10
+ }
11
+
12
+ export interface SidebarSection {
13
+ label: string;
14
+ items?: SidebarItem[];
15
+ slug?: string;
16
+ autogenerated?: {
17
+ directory: string;
18
+ };
19
+ badge?: {
20
+ text: string;
21
+ color: string;
22
+ };
23
+ collapsed?: boolean;
24
+ }
25
+
26
+ export interface CustomDocsNavProps {
27
+ sidebarItems: SidebarSection[];
28
+ currentPath: string;
29
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { getNavigationItems } from '@enterprise/custom-documentation/utils/custom-docs';
3
+ import CustomDocsNavWrapper from '@enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper';
4
+
5
+ const currentPath = Astro.url.pathname;
6
+ const sidebarItems = await getNavigationItems();
7
+ ---
8
+
9
+ <CustomDocsNavWrapper client:load sidebarItems={sidebarItems} currentPath={currentPath} />
@@ -115,8 +115,8 @@ function groupChildrenByType(parentNode: TreeNode) {
115
115
  acc[n.type].push(n);
116
116
  });
117
117
 
118
- // Collapse all messages
119
- const AUTO_EXPANDED_TYPES = ['domains', 'services', 'channels'];
118
+ // Collapse everything except domains
119
+ const AUTO_EXPANDED_TYPES = ['domains'];
120
120
 
121
121
  parentNode.children = Object.entries(acc)
122
122
  // Order label nodes by RESOURCE_TYPES