@eventcatalog/core 3.0.0-beta.2 → 3.0.0-beta.21

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 (103) hide show
  1. package/README.md +10 -0
  2. package/dist/__mocks__/astro-content.cjs +32 -0
  3. package/dist/__mocks__/astro-content.d.cts +13 -0
  4. package/dist/__mocks__/astro-content.d.ts +13 -0
  5. package/dist/__mocks__/astro-content.js +7 -0
  6. package/dist/analytics/analytics.cjs +1 -1
  7. package/dist/analytics/analytics.js +2 -2
  8. package/dist/analytics/log-build.cjs +1 -1
  9. package/dist/analytics/log-build.js +3 -3
  10. package/dist/{chunk-JSONCD7V.js → chunk-2FUEBPD3.js} +1 -1
  11. package/dist/{chunk-3W6JYTHP.js → chunk-HABY2LVH.js} +6 -2
  12. package/dist/{chunk-H4QHE5YZ.js → chunk-KQAMO3R4.js} +1 -1
  13. package/dist/chunk-Q6KRYWPV.js +44 -0
  14. package/dist/{chunk-PQL6O5YA.js → chunk-RRP2B7BL.js} +1 -1
  15. package/dist/constants.cjs +1 -1
  16. package/dist/constants.js +1 -1
  17. package/dist/eventcatalog.cjs +84 -65
  18. package/dist/eventcatalog.config.d.cts +4 -0
  19. package/dist/eventcatalog.config.d.ts +4 -0
  20. package/dist/eventcatalog.js +45 -57
  21. package/dist/generate.cjs +48 -2
  22. package/dist/generate.js +3 -1
  23. package/dist/utils/cli-logger.cjs +82 -0
  24. package/dist/utils/cli-logger.d.cts +10 -0
  25. package/dist/utils/cli-logger.d.ts +10 -0
  26. package/dist/utils/cli-logger.js +7 -0
  27. package/eventcatalog/astro.config.mjs +4 -1
  28. package/eventcatalog/integrations/ecstudio-watcher.mjs +1 -1
  29. package/eventcatalog/integrations/eventcatalog-features.ts +69 -0
  30. package/eventcatalog/public/icons/asyncapi-black.svg +2 -0
  31. package/eventcatalog/public/icons/graphql-black.svg +1 -0
  32. package/eventcatalog/public/icons/openapi-black.svg +1 -0
  33. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +821 -0
  34. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +24 -0
  35. package/eventcatalog/src/components/Grids/DomainGrid.tsx +1 -3
  36. package/eventcatalog/src/components/Grids/MessageGrid.tsx +8 -8
  37. package/eventcatalog/src/components/Header.astro +25 -5
  38. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -3
  39. package/eventcatalog/src/components/Search/Search.astro +2 -2
  40. package/eventcatalog/src/components/Search/SearchModal.tsx +16 -7
  41. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +9 -2
  42. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +7 -6
  43. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +6 -3
  44. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +1 -0
  45. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +23 -8
  46. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +57 -11
  47. package/eventcatalog/src/content.config.ts +1 -10
  48. package/eventcatalog/src/enterprise/ai/chat-api.ts +262 -0
  49. package/eventcatalog/src/enterprise/auth/[...auth].ts +3 -0
  50. package/eventcatalog/src/enterprise/auth/login.astro +420 -0
  51. package/eventcatalog/src/enterprise/collections/index.ts +0 -1
  52. package/eventcatalog/src/layouts/Footer.astro +8 -5
  53. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +30 -19
  54. package/eventcatalog/src/pages/_index.astro +8 -9
  55. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -3
  56. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +7 -7
  57. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
  58. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +5 -5
  59. package/eventcatalog/src/pages/docs/teams/[id].mdx.ts +36 -0
  60. package/eventcatalog/src/pages/docs/users/[id].mdx.ts +36 -0
  61. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +178 -0
  62. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -155
  63. package/eventcatalog/src/remark-plugins/directives.ts +30 -9
  64. package/eventcatalog/src/utils/collections/schemas.ts +31 -7
  65. package/eventcatalog/src/utils/feature.ts +8 -4
  66. package/eventcatalog/src/utils/resource-files.ts +86 -0
  67. package/package.json +12 -15
  68. package/default-files-for-collections/changelogs.md +0 -5
  69. package/default-files-for-collections/channels.md +0 -8
  70. package/default-files-for-collections/commands.md +0 -8
  71. package/default-files-for-collections/domains.md +0 -8
  72. package/default-files-for-collections/events.md +0 -8
  73. package/default-files-for-collections/flows.md +0 -11
  74. package/default-files-for-collections/queries.md +0 -8
  75. package/default-files-for-collections/services.md +0 -8
  76. package/default-files-for-collections/ubiquitousLanguages.md +0 -7
  77. package/eventcatalog/src/enterprise/collections/chat-prompts.ts +0 -32
  78. package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +0 -60
  79. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +0 -414
  80. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatSidebar.tsx +0 -169
  81. package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +0 -244
  82. package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +0 -211
  83. package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +0 -176
  84. package/eventcatalog/src/enterprise/eventcatalog-chat/components/default-prompts.ts +0 -93
  85. package/eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx +0 -143
  86. package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +0 -387
  87. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts +0 -59
  88. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +0 -104
  89. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts +0 -140
  90. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts +0 -28
  91. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts +0 -41
  92. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts +0 -26
  93. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts +0 -61
  94. package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +0 -50
  95. package/eventcatalog/src/pages/auth/login.astro +0 -280
  96. package/eventcatalog/src/pages/chat/feature.astro +0 -179
  97. package/eventcatalog/src/pages/chat/index.astro +0 -10
  98. package/eventcatalog/src/pages/nav-index.json.ts +0 -30
  99. /package/eventcatalog/src/{pages → enterprise}/auth/error.astro +0 -0
  100. /package/eventcatalog/src/{middleware-auth.ts → enterprise/auth/middleware/middleware-auth.ts} +0 -0
  101. /package/eventcatalog/src/{middleware.ts → enterprise/auth/middleware/middleware.ts} +0 -0
  102. /package/eventcatalog/src/{pages/unauthorized/index.astro → enterprise/auth/unauthorized.astro} +0 -0
  103. /package/eventcatalog/src/{pages → enterprise}/plans/index.astro +0 -0
@@ -0,0 +1,24 @@
1
+ import { useState } from 'react';
2
+ import { Sparkles } from 'lucide-react';
3
+ import ChatPanel from './ChatPanel';
4
+
5
+ const ChatPanelButton = () => {
6
+ const [isOpen, setIsOpen] = useState(false);
7
+
8
+ return (
9
+ <>
10
+ <button
11
+ onClick={() => setIsOpen(true)}
12
+ className="flex items-center gap-1.5 px-4 py-1.5 rounded-md bg-white hover:bg-gray-50 ring-1 ring-inset ring-gray-300 shadow-sm transition-colors text-sm ml-[-1px]"
13
+ aria-label="Open AI Assistant"
14
+ >
15
+ <Sparkles size={14} className="text-purple-500" />
16
+ <span className="font-light text-gray-600">Ask AI</span>
17
+ </button>
18
+
19
+ <ChatPanel isOpen={isOpen} onClose={() => setIsOpen(false)} />
20
+ </>
21
+ );
22
+ };
23
+
24
+ export default ChatPanelButton;
@@ -11,7 +11,7 @@ import {
11
11
  ArrowsPointingOutIcon,
12
12
  } from '@heroicons/react/24/outline';
13
13
  import { buildUrl } from '@utils/url-builder';
14
- import { BoxIcon, ArrowRight, ArrowLeft } from 'lucide-react';
14
+ import { BoxIcon } from 'lucide-react';
15
15
 
16
16
  // ============================================
17
17
  // Types
@@ -135,7 +135,6 @@ const ServiceCard = memo(({ service }: { service: any }) => {
135
135
  {/* Receives (Inbound) */}
136
136
  <div className="flex-1 bg-blue-50 border border-blue-200 rounded-lg p-3">
137
137
  <div className="flex items-center gap-1.5 mb-2">
138
- <ArrowRight className="h-3.5 w-3.5 text-blue-500" />
139
138
  <span className="text-xs font-semibold text-blue-700 uppercase">Inbound Messages</span>
140
139
  <span className="text-xs text-blue-500">({receives.length})</span>
141
140
  </div>
@@ -162,7 +161,6 @@ const ServiceCard = memo(({ service }: { service: any }) => {
162
161
  {/* Sends (Outbound) */}
163
162
  <div className="flex-1 bg-green-50 border border-green-200 rounded-lg p-3">
164
163
  <div className="flex items-center gap-1.5 mb-2">
165
- <ArrowLeft className="h-3.5 w-3.5 text-green-500" />
166
164
  <span className="text-xs font-semibold text-green-700 uppercase">Outbound Messages</span>
167
165
  <span className="text-xs text-green-500">({sends.length})</span>
168
166
  </div>
@@ -41,9 +41,9 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
41
41
  );
42
42
 
43
43
  return (
44
- <div className={`rounded-xl overflow-hidden bg-pink-50 p-8 border-2 border-dashed border-pink-300`}>
44
+ <div className={`rounded-xl overflow-hidden bg-white p-8 border-2 border-dashed border-pink-300`}>
45
45
  {/* Service Title */}
46
- <div className="flex items-center gap-2 mb-8">
46
+ {/* <div className="flex items-center gap-2 mb-8">
47
47
  <ServerIcon className="h-6 w-6 text-pink-500" />
48
48
  <h2 className="text-2xl font-semibold text-gray-900">{service.data.name}</h2>
49
49
  <div className="flex gap-2 ml-auto">
@@ -60,7 +60,7 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
60
60
  Read documentation
61
61
  </a>
62
62
  </div>
63
- </div>
63
+ </div> */}
64
64
 
65
65
  <div className="grid grid-cols-3 gap-8 relative">
66
66
  {/* Left Column - Receives Messages & Reads From Containers */}
@@ -70,7 +70,7 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
70
70
  <div className="mb-6">
71
71
  <h2 className={`font-semibold text-gray-900 flex items-center gap-2 ${embeded ? 'text-sm' : 'text-xl'}`}>
72
72
  <ServerIcon className="h-5 w-5 text-blue-500" />
73
- Receives ({receives.length})
73
+ Inbound Messages ({receives.length})
74
74
  </h2>
75
75
  </div>
76
76
  {receives.length > 0 ? (
@@ -124,7 +124,7 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
124
124
  </div>
125
125
 
126
126
  {/* Service Information (Center) */}
127
- <div className="bg-white border-2 border-pink-100 rounded-lg p-6 flex flex-col justify-center">
127
+ <div className="bg-pink-50 border-2 border-pink-100 rounded-lg p-6 flex flex-col justify-center">
128
128
  <div className="flex flex-col items-center gap-4">
129
129
  <ServerIcon className="h-12 w-12 text-pink-500" />
130
130
  <p className="text-xl font-semibold text-gray-900 text-center">{service.data.name}</p>
@@ -133,11 +133,11 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
133
133
  <div className="w-full grid grid-cols-2 gap-3 mt-2">
134
134
  <div className="text-center p-3 bg-blue-50 rounded-lg border border-blue-200">
135
135
  <div className="text-2xl font-bold text-blue-600">{receives.length}</div>
136
- <div className="text-xs text-gray-600 mt-1">Receives</div>
136
+ <div className="text-xs text-gray-600 mt-1">Inbound Messages</div>
137
137
  </div>
138
138
  <div className="text-center p-3 bg-green-50 rounded-lg border border-green-200">
139
139
  <div className="text-2xl font-bold text-green-600">{sends.length}</div>
140
- <div className="text-xs text-gray-600 mt-1">Sends</div>
140
+ <div className="text-xs text-gray-600 mt-1">Outbound Messages</div>
141
141
  </div>
142
142
  {readsFrom.length > 0 && (
143
143
  <div className="text-center p-3 bg-orange-50 rounded-lg border border-orange-200">
@@ -168,7 +168,7 @@ export default function MessageGridV2({ service, embeded = false }: MessageGridV
168
168
  <div className="mb-6">
169
169
  <h2 className={`font-semibold text-gray-900 flex items-center gap-2 ${embeded ? 'text-sm' : 'text-xl'}`}>
170
170
  <ServerIcon className="h-5 w-5 text-emerald-500" />
171
- Sends ({sends.length})
171
+ Outbound Messages ({sends.length})
172
172
  </h2>
173
173
  </div>
174
174
  {sends.length > 0 ? (
@@ -2,10 +2,11 @@
2
2
  import catalog from '@utils/eventcatalog-config/catalog';
3
3
  import Search from '@components/Search/Search.astro';
4
4
  import { buildUrl } from '@utils/url-builder';
5
- import { showEventCatalogBranding, showCustomBranding } from '@utils/feature';
5
+ import { showEventCatalogBranding, showCustomBranding, isEventCatalogChatEnabled } from '@utils/feature';
6
6
  import { getSession } from 'auth-astro/server';
7
7
  import { isAuthEnabled, isSSR } from '@utils/feature';
8
8
  import { EnvironmentDropdown } from './EnvironmentDropdown';
9
+ import ChatPanelButton from './ChatPanel/ChatPanelButton';
9
10
 
10
11
  let session = null;
11
12
  if (isAuthEnabled()) {
@@ -42,8 +43,11 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
42
43
  </a>
43
44
  </div>
44
45
 
45
- <div class="hidden lg:block flex-grow -ml-1">
46
- <Search />
46
+ <div class="hidden lg:flex flex-grow -ml-1 items-center">
47
+ <div class="flex-grow max-w-xl">
48
+ <Search />
49
+ </div>
50
+ {isEventCatalogChatEnabled() && <ChatPanelButton client:load />}
47
51
  </div>
48
52
 
49
53
  <div class="hidden md:block w-3/12">
@@ -60,6 +64,7 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
60
64
  class="flex items-center focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-400 rounded-full"
61
65
  aria-expanded="false"
62
66
  aria-haspopup="true"
67
+ aria-label="User menu"
63
68
  >
64
69
  {session.user?.image && !session.user?.image?.includes('googleusercontent.com') ? (
65
70
  <img
@@ -117,16 +122,28 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
117
122
  <a
118
123
  href="https://discord.com/invite/3rjaZMmrAm"
119
124
  class="block p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
125
+ aria-label="Join our Discord community"
120
126
  >
121
- <img src={buildUrl('/icons/discord.svg', true)} class="h-6 w-6 hover:opacity-100 transition-opacity" />
127
+ <img
128
+ src={buildUrl('/icons/discord.svg', true)}
129
+ alt=""
130
+ class="h-6 w-6 hover:opacity-100 transition-opacity"
131
+ aria-hidden="true"
132
+ />
122
133
  </a>
123
134
  </li>
124
135
  <li>
125
136
  <a
126
137
  href="https://github.com/event-catalog/eventcatalog"
127
138
  class="block p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
139
+ aria-label="View EventCatalog on GitHub"
128
140
  >
129
- <img src={buildUrl('/icons/github.svg', true)} class="h-6 w-6 hover:opacity-100 transition-opacity" />
141
+ <img
142
+ src={buildUrl('/icons/github.svg', true)}
143
+ alt=""
144
+ class="h-6 w-6 hover:opacity-100 transition-opacity"
145
+ aria-hidden="true"
146
+ />
130
147
  </a>
131
148
  </li>
132
149
  </ul>
@@ -137,10 +154,13 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
137
154
  <a
138
155
  href={repositoryUrl}
139
156
  class="block p-1.5 rounded-lg hover:bg-gray-100 transition-colors focus:outline-none"
157
+ aria-label="View repository on GitHub"
140
158
  >
141
159
  <img
142
160
  src={buildUrl('/icons/github.svg', true)}
161
+ alt=""
143
162
  class="h-6 w-6 opacity-70 hover:opacity-100 transition-opacity"
163
+ aria-hidden="true"
144
164
  />
145
165
  </a>
146
166
  </li>
@@ -232,10 +232,21 @@ const NodeGraphBuilder = ({
232
232
  };
233
233
 
234
234
  // animate messages, between views
235
+ // URL parameter takes priority over localStorage
235
236
  useEffect(() => {
236
- const storedAnimateMessages = localStorage.getItem('EventCatalog:animateMessages');
237
- if (storedAnimateMessages !== null) {
238
- setAnimateMessages(storedAnimateMessages === 'true');
237
+ const urlParams = new URLSearchParams(window.location.search);
238
+ const animateParam = urlParams.get('animate');
239
+
240
+ if (animateParam === 'true') {
241
+ setAnimateMessages(true);
242
+ } else if (animateParam === 'false') {
243
+ setAnimateMessages(false);
244
+ } else {
245
+ // Fall back to localStorage if no URL parameter
246
+ const storedAnimateMessages = localStorage.getItem('EventCatalog:animateMessages');
247
+ if (storedAnimateMessages !== null) {
248
+ setAnimateMessages(storedAnimateMessages === 'true');
249
+ }
239
250
  }
240
251
  }, []);
241
252
 
@@ -4,7 +4,7 @@ import SearchModal from './SearchModal.tsx';
4
4
  ---
5
5
 
6
6
  <div>
7
- <div class="relative flex items-center w-10/12">
7
+ <div class="relative flex items-center w-full pr-4">
8
8
  <input
9
9
  id="search-dummy-input"
10
10
  type="text"
@@ -14,7 +14,7 @@ import SearchModal from './SearchModal.tsx';
14
14
  class="block w-full rounded-md caret-transparent border-0 py-1.5 pr-14 pl-10 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 font-light sm:text-sm sm:leading-6 px-4"
15
15
  />
16
16
  <MagnifyingGlassIcon className="absolute inset-y-0 left-0 h-9 w-8 flex items-center pl-4 text-gray-400" />
17
- <div class="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
17
+ <div class="absolute inset-y-0 right-0 flex py-1.5 pr-6">
18
18
  <kbd class="inline-flex items-center rounded px-1 font-sans text-xs text-gray-400">⌘K</kbd>
19
19
  </div>
20
20
  </div>
@@ -20,6 +20,7 @@ import {
20
20
  ArrowUturnLeftIcon,
21
21
  StarIcon,
22
22
  Square2StackIcon,
23
+ ArrowsRightLeftIcon,
23
24
  } from '@heroicons/react/24/outline';
24
25
  import { StarIcon as StarIconSolid, CircleStackIcon } from '@heroicons/react/24/solid';
25
26
  import { useStore } from '@nanostores/react';
@@ -33,7 +34,7 @@ const typeIcons: any = {
33
34
  Command: ChatBubbleLeftIcon,
34
35
  Query: QueryIcon,
35
36
  Entity: CubeIcon,
36
- Channel: QueueListIcon,
37
+ Channel: ArrowsRightLeftIcon,
37
38
  Team: UserGroupIcon,
38
39
  User: UserIcon,
39
40
  Language: BookOpenIcon,
@@ -68,8 +69,6 @@ function classNames(...classes: (string | boolean | undefined)[]) {
68
69
 
69
70
  // Helper to construct URL from key if href is missing
70
71
  const getUrlForItem = (node: any, key: string) => {
71
- if (node.href) return node.href;
72
-
73
72
  const parts = key.split(':');
74
73
  if (parts.length < 2) return null; // Need at least type:id
75
74
 
@@ -83,8 +82,16 @@ const getUrlForItem = (node: any, key: string) => {
83
82
  // Only show items that have a version to avoid duplicates
84
83
  if (!version) return null;
85
84
 
85
+ // If node has href, use it, otherwise construct from key
86
+ if (node.href) return node.href;
87
+
86
88
  // Pluralize type for URL if needed
87
- const pluralType = ['event', 'command', 'query', 'domain', 'service', 'flow', 'container'].includes(type) ? type + 's' : type; // users/teams already have href usually, but safe fallback
89
+ let pluralType = type;
90
+ if (['event', 'command', 'domain', 'service', 'flow', 'container', 'channel'].includes(type)) {
91
+ pluralType = type + 's';
92
+ } else if (type === 'query') {
93
+ pluralType = 'queries';
94
+ }
88
95
 
89
96
  return `/docs/${pluralType}/${id}/${version}`;
90
97
  };
@@ -171,6 +178,7 @@ export default function SearchModal() {
171
178
  Team: 0,
172
179
  Container: 0,
173
180
  Design: 0,
181
+ Channel: 0,
174
182
  };
175
183
 
176
184
  itemsToCount.forEach((item) => {
@@ -195,6 +203,7 @@ export default function SearchModal() {
195
203
  if (counts.Service > 0) dynamicFilters.push({ id: 'Service', name: `Services (${counts.Service})` });
196
204
  if (counts.Message > 0) dynamicFilters.push({ id: 'Message', name: `Messages (${counts.Message})` });
197
205
  if (counts.Container > 0) dynamicFilters.push({ id: 'Container', name: `Containers (${counts.Container})` });
206
+ if (counts.Channel > 0) dynamicFilters.push({ id: 'Channel', name: `Channels (${counts.Channel})` });
198
207
  if (counts.Design > 0) dynamicFilters.push({ id: 'Design', name: `Designs (${counts.Design})` });
199
208
  if (counts.Team > 0) dynamicFilters.push({ id: 'Team', name: `Teams & Users (${counts.Team})` });
200
209
 
@@ -224,7 +233,7 @@ export default function SearchModal() {
224
233
  const filteredItems = useMemo(() => {
225
234
  if (query === '') {
226
235
  // Show favorites when search is empty
227
- if (favorites.length > 0) {
236
+ if (favorites.length > 0 && activeFilter === 'all') {
228
237
  return favorites
229
238
  .slice(0, 5)
230
239
  .map((fav) => {
@@ -375,14 +384,14 @@ export default function SearchModal() {
375
384
  <p className={classNames('text-sm font-medium', active ? 'text-gray-900' : 'text-gray-700')}>
376
385
  {item.name}
377
386
  </p>
378
- <div className="flex items-start gap-2">
387
+ <div className="flex items-center gap-2">
379
388
  <p
380
389
  className={classNames('text-sm flex-shrink-0', active ? 'text-gray-700' : 'text-gray-500')}
381
390
  >
382
391
  {item.type}
383
392
  </p>
384
393
  {item.rawNode.summary && (
385
- <p className={classNames('text-xs truncate', active ? 'text-gray-600' : 'text-gray-400')}>
394
+ <p className={classNames('text-sm truncate', active ? 'text-gray-600' : 'text-gray-400')}>
386
395
  • {item.rawNode.summary}
387
396
  </p>
388
397
  )}
@@ -14,6 +14,8 @@ import {
14
14
  Database,
15
15
  Waypoints,
16
16
  SquareMousePointer,
17
+ ListOrdered,
18
+ ArrowLeftRight,
17
19
  } from 'lucide-react';
18
20
  import type { NavNode } from './sidebar-builder';
19
21
 
@@ -28,6 +30,7 @@ const getBadgeClasses = (badge: string): string => {
28
30
  query: 'bg-purple-100 text-purple-700',
29
31
  message: 'bg-indigo-100 text-indigo-700',
30
32
  design: 'bg-teal-100 text-teal-700',
33
+ channel: 'bg-indigo-100 text-indigo-700',
31
34
  };
32
35
  return badgeColors[badge.toLowerCase()] || 'bg-gray-100 text-gray-600';
33
36
  };
@@ -77,6 +80,7 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
77
80
  };
78
81
 
79
82
  const filterTypes = [
83
+ { key: 'channel', label: 'Channels', badge: 'Channel', icon: ArrowLeftRight },
80
84
  { key: 'command', label: 'Commands', badge: 'Command', icon: MessageSquare },
81
85
  { key: 'container', label: 'Data Stores', badge: 'Container', icon: Database },
82
86
  { key: 'design', label: 'Designs', badge: 'Design', icon: SquareMousePointer },
@@ -174,8 +178,9 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
174
178
  <button
175
179
  onClick={clearSearch}
176
180
  className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
181
+ aria-label="Clear search"
177
182
  >
178
- <X className="w-4 h-4" />
183
+ <X className="w-4 h-4" aria-hidden="true" />
179
184
  </button>
180
185
  )}
181
186
  </div>
@@ -189,8 +194,10 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
189
194
  ? 'bg-purple-50 border-purple-200 text-purple-600'
190
195
  : 'bg-gray-50 border-gray-200 text-gray-400 hover:text-gray-600 hover:bg-gray-100'
191
196
  )}
197
+ aria-label="Filter search results"
198
+ aria-expanded={showFilterDropdown}
192
199
  >
193
- <SlidersHorizontal className="w-4 h-4" />
200
+ <SlidersHorizontal className="w-4 h-4" aria-hidden="true" />
194
201
  {searchFilters.size > 0 && (
195
202
  <span className="absolute -top-1 -right-1 w-4 h-4 bg-purple-600 text-white text-[10px] font-bold rounded-full flex items-center justify-center">
196
203
  {searchFilters.size}
@@ -65,6 +65,12 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
65
65
  },
66
66
  ].filter(Boolean) as ChildRef[],
67
67
  },
68
+ renderSubDomains && {
69
+ type: 'group',
70
+ title: 'Subdomains',
71
+ icon: 'Boxes',
72
+ pages: subDomains.map((domain) => `domain:${(domain as any).data.id}:${(domain as any).data.version}`),
73
+ },
68
74
  hasFlows && {
69
75
  type: 'group',
70
76
  title: 'Flows',
@@ -81,12 +87,7 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
81
87
  href: buildUrl(`/docs/entities/${(entity as any).data.id}/${(entity as any).data.version}`),
82
88
  })),
83
89
  },
84
- renderSubDomains && {
85
- type: 'group',
86
- title: 'Subdomains',
87
- icon: 'Boxes',
88
- pages: subDomains.map((domain) => `domain:${(domain as any).data.id}:${(domain as any).data.version}`),
89
- },
90
+
90
91
  ...(hasResourceGroups ? buildResourceGroupSections(resourceGroups, context) : []),
91
92
  renderServices && {
92
93
  type: 'group',
@@ -80,21 +80,24 @@ export const buildServiceNode = (service: CollectionEntry<'services'>, owners: a
80
80
  pages: [
81
81
  ...openAPISpecifications.map((specification) => ({
82
82
  type: 'item',
83
- title: `${specification.name} (OpenAPI)`,
83
+ title: `${specification.name}`,
84
+ leftIcon: '/icons/openapi-black.svg',
84
85
  href: buildUrl(
85
86
  `/docs/services/${service.data.id}/${service.data.version}/spec/${specification.filenameWithoutExtension}`
86
87
  ),
87
88
  })),
88
89
  ...asyncAPISpecifications.map((specification) => ({
89
90
  type: 'item',
90
- title: `${specification.name} (AsyncAPI)`,
91
+ title: `${specification.name}`,
92
+ leftIcon: '/icons/asyncapi-black.svg',
91
93
  href: buildUrl(
92
94
  `/docs/services/${service.data.id}/${service.data.version}/asyncapi/${specification.filenameWithoutExtension}`
93
95
  ),
94
96
  })),
95
97
  ...graphQLSpecifications.map((specification) => ({
96
98
  type: 'item',
97
- title: `${specification.name} (GraphQL)`,
99
+ title: `${specification.name}`,
100
+ leftIcon: '/icons/graphql-black.svg',
98
101
  href: buildUrl(
99
102
  `/docs/services/${service.data.id}/${service.data.version}/graphql/${specification.filenameWithoutExtension}`
100
103
  ),
@@ -17,6 +17,7 @@ export type NavNode = {
17
17
  type: 'group' | 'item';
18
18
  title: string;
19
19
  icon?: string; // Lucide icon name
20
+ leftIcon?: string; // Path to SVG icon shown on the left of the label
20
21
  href?: string; // URL (for leaf items)
21
22
  external?: boolean; // If true, the item will open in a new tab
22
23
  pages?: ChildRef[]; // Can mix keys and inline nodes
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useMemo } from 'react';
4
4
  import * as LucideIcons from 'lucide-react';
5
- import { ChevronRight, ChevronLeft, ChevronDown, Home, Star } from 'lucide-react';
5
+ import { ChevronRight, ChevronLeft, ChevronDown, Home, Star, FileQuestion } from 'lucide-react';
6
6
  import type { NavigationData, NavNode, ChildRef } from './sidebar-builder';
7
7
  import SearchBar from './SearchBar';
8
8
  import { saveState, loadState, saveCollapsedSections, loadCollapsedSections } from './storage';
@@ -25,6 +25,7 @@ const getBadgeClasses = (badge: string): string => {
25
25
  query: 'bg-purple-100 text-purple-700',
26
26
  message: 'bg-indigo-100 text-indigo-700',
27
27
  design: 'bg-teal-100 text-teal-700',
28
+ channel: 'bg-indigo-100 text-indigo-700',
28
29
  };
29
30
  return badgeColors[badge.toLowerCase()] || 'bg-gray-100 text-gray-600';
30
31
  };
@@ -775,6 +776,7 @@ export default function NestedSideBar() {
775
776
  <IconComponent className="w-4 h-4" />
776
777
  </span>
777
778
  )}
779
+ {item.leftIcon && <img src={item.leftIcon} alt="" className="w-4 h-4 flex-shrink-0" />}
778
780
  <span
779
781
  className={cn(
780
782
  'text-[13px] truncate',
@@ -786,17 +788,17 @@ export default function NestedSideBar() {
786
788
  </div>
787
789
  <div className="flex items-center gap-1 flex-shrink-0">
788
790
  {canFavorite && (
789
- <button
791
+ <div
790
792
  onClick={handleStarClick}
791
793
  className={cn(
792
- 'flex items-center justify-center w-5 h-5 rounded transition-colors',
794
+ 'flex items-center justify-center w-5 h-5 rounded transition-colors cursor-pointer',
793
795
  isFav
794
796
  ? 'text-amber-400 hover:text-amber-500'
795
797
  : 'text-gray-300 opacity-0 group-hover:opacity-100 hover:text-amber-400'
796
798
  )}
797
799
  >
798
800
  <Star className={cn('w-3.5 h-3.5', isFav && 'fill-current')} />
799
- </button>
801
+ </div>
800
802
  )}
801
803
  {itemHasChildren && (
802
804
  <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-black group-hover:translate-x-0.5 transition-transform">
@@ -1038,15 +1040,15 @@ export default function NestedSideBar() {
1038
1040
  {fav.badge}
1039
1041
  </span>
1040
1042
  )}
1041
- <button
1043
+ <div
1042
1044
  onClick={(e) => {
1043
1045
  e.stopPropagation();
1044
1046
  if (node) toggleFavorite(fav.nodeKey, node);
1045
1047
  }}
1046
- className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors"
1048
+ className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1047
1049
  >
1048
1050
  <Star className="w-3.5 h-3.5 fill-current" />
1049
- </button>
1051
+ </div>
1050
1052
  {node?.pages && node.pages.length > 0 && (
1051
1053
  <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-black">
1052
1054
  <ChevronRight className="w-4 h-4" />
@@ -1060,7 +1062,20 @@ export default function NestedSideBar() {
1060
1062
  </div>
1061
1063
  )}
1062
1064
 
1063
- {renderEntries(currentLevel.entries)}
1065
+ {/* Empty State */}
1066
+ {currentLevel.entries.length === 0 && favorites.length === 0 && (
1067
+ <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1068
+ <div className="mb-4 p-3 rounded-full bg-gray-100">
1069
+ <FileQuestion className="w-8 h-8 text-gray-400" />
1070
+ </div>
1071
+ <h3 className="text-sm font-semibold text-gray-900 mb-2">Your catalog is empty</h3>
1072
+ <p className="text-xs text-gray-500 leading-relaxed max-w-[240px]">
1073
+ Navigation will appear here when you add resources to your EventCatalog.
1074
+ </p>
1075
+ </div>
1076
+ )}
1077
+
1078
+ {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1064
1079
  </nav>
1065
1080
  </>
1066
1081
  )}
@@ -15,6 +15,7 @@ import { buildContainerNode } from './builders/container';
15
15
  import { buildFlowNode } from './builders/flow';
16
16
  import config from '@config';
17
17
  import { getDesigns } from '@utils/collections/designs';
18
+ import { getChannels } from '@utils/collections/channels';
18
19
 
19
20
  export type { NavigationData, NavNode, ChildRef };
20
21
 
@@ -29,16 +30,18 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
29
30
  return memoryCache;
30
31
  }
31
32
 
32
- const [domains, services, { events, commands, queries }, containers, flows, users, teams, designs] = await Promise.all([
33
- getDomains({ getAllVersions: false, includeServicesInSubdomains: false }),
34
- getServices({ getAllVersions: false }),
35
- getMessages({ getAllVersions: false }),
36
- getContainers({ getAllVersions: false }),
37
- getFlows({ getAllVersions: false }),
38
- getUsers(),
39
- getTeams(),
40
- getDesigns(),
41
- ]);
33
+ const [domains, services, { events, commands, queries }, containers, flows, users, teams, designs, channels] =
34
+ await Promise.all([
35
+ getDomains({ getAllVersions: false, includeServicesInSubdomains: false }),
36
+ getServices({ getAllVersions: false }),
37
+ getMessages({ getAllVersions: false }),
38
+ getContainers({ getAllVersions: false }),
39
+ getFlows({ getAllVersions: false }),
40
+ getUsers(),
41
+ getTeams(),
42
+ getDesigns(),
43
+ getChannels({ getAllVersions: false }),
44
+ ]);
42
45
 
43
46
  // Calculate derived lists to avoid extra fetches
44
47
  const allSubDomainIds = new Set(domains.flatMap((d) => (d.data.domains || []).map((sd: any) => sd.data.id)));
@@ -178,6 +181,30 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
178
181
  {} as Record<string, NavNode>
179
182
  );
180
183
 
184
+ const channelNodes = channels.reduce(
185
+ (acc, channel) => {
186
+ acc[`channel:${channel.data.id}:${channel.data.version}`] = {
187
+ type: 'item',
188
+ title: channel.data.name,
189
+ badge: 'Channel',
190
+ summary: channel.data.summary,
191
+ href: buildUrl(`/docs/${channel.collection}/${channel.data.id}/${channel.data.version}`),
192
+ };
193
+
194
+ if (channel.data.latestVersion === channel.data.version) {
195
+ acc[`channel:${channel.data.id}`] = {
196
+ type: 'item',
197
+ title: channel.data.name,
198
+ badge: 'Channel',
199
+ summary: channel.data.summary,
200
+ href: buildUrl(`/docs/${channel.collection}/${channel.data.id}/${channel.data.version}`),
201
+ };
202
+ }
203
+ return acc;
204
+ },
205
+ {} as Record<string, NavNode>
206
+ );
207
+
181
208
  const teamNodes = teams.reduce(
182
209
  (acc, team) => {
183
210
  acc[`team:${team.data.id}`] = {
@@ -273,6 +300,13 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
273
300
  pages: users.map((user) => `user:${user.data.id}`),
274
301
  });
275
302
 
303
+ const channelList = createLeaf(channels, {
304
+ type: 'item',
305
+ title: 'Channels',
306
+ icon: 'ArrowRightLeft',
307
+ pages: channels.map((channel) => `channel:${channel.data.id}:${channel.data.version}`),
308
+ });
309
+
276
310
  const messagesChildren = ['list:events', 'list:commands', 'list:queries'].filter(
277
311
  (key, index) => [eventsList, commandsList, queriesList][index] !== undefined
278
312
  );
@@ -303,12 +337,22 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
303
337
  'list:domains',
304
338
  'list:services',
305
339
  'list:messages',
340
+ 'list:channels',
306
341
  'list:flows',
307
342
  'list:containers',
308
343
  'list:designs',
309
344
  'list:people',
310
345
  ];
311
- const allChildrenNodes = [domainsList, servicesList, messagesList, flowsList, containersList, designsList, peopleList];
346
+ const allChildrenNodes = [
347
+ domainsList,
348
+ servicesList,
349
+ messagesList,
350
+ channelList,
351
+ flowsList,
352
+ containersList,
353
+ designsList,
354
+ peopleList,
355
+ ];
312
356
 
313
357
  const validAllChildren = allChildrenKeys.filter((_, idx) => allChildrenNodes[idx] !== undefined);
314
358
 
@@ -334,6 +378,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
334
378
  ...(designsList ? { 'list:designs': designsList } : {}),
335
379
  ...(teamsList ? { 'list:teams': teamsList } : {}),
336
380
  ...(usersList ? { 'list:users': usersList } : {}),
381
+ ...(channelList ? { 'list:channels': channelList as NavNode } : {}),
337
382
  ...(peopleList ? { 'list:people': peopleList as NavNode } : {}),
338
383
  ...(allList ? { 'list:all': allList as NavNode } : {}),
339
384
  };
@@ -343,6 +388,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
343
388
  ...domainNodes,
344
389
  ...serviceNodes,
345
390
  ...messageNodes,
391
+ ...channelNodes,
346
392
  ...containerNodes,
347
393
  ...flowNodes,
348
394
  ...userNodes,