@eventcatalog/core 3.0.0-beta.1 → 3.0.0-beta.11

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 (32) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-M6QG3NPM.js → chunk-EPAI7P6H.js} +1 -1
  6. package/dist/{chunk-3W6JYTHP.js → chunk-HHZD3IEE.js} +6 -2
  7. package/dist/{chunk-KE2IUVK6.js → chunk-NGLFVSLZ.js} +1 -1
  8. package/dist/chunk-SI5FCV46.js +44 -0
  9. package/dist/{chunk-FH5AN4Z6.js → chunk-WYPVB6OY.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +66 -20
  13. package/dist/eventcatalog.js +27 -16
  14. package/dist/generate.cjs +48 -2
  15. package/dist/generate.js +3 -1
  16. package/dist/utils/cli-logger.cjs +82 -0
  17. package/dist/utils/cli-logger.d.cts +10 -0
  18. package/dist/utils/cli-logger.d.ts +10 -0
  19. package/dist/utils/cli-logger.js +7 -0
  20. package/eventcatalog/integrations/ecstudio-watcher.mjs +1 -1
  21. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -2
  22. package/eventcatalog/src/components/Grids/MessageGrid.tsx +8 -8
  23. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +45 -3
  24. package/eventcatalog/src/components/Search/SearchModal.tsx +16 -7
  25. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +4 -0
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +7 -6
  27. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +22 -8
  28. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +57 -11
  29. package/eventcatalog/src/pages/_index.astro +5 -7
  30. package/eventcatalog/src/pages/auth/login.astro +2 -2
  31. package/eventcatalog/src/remark-plugins/directives.ts +30 -9
  32. package/package.json +3 -2
@@ -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 ? (
@@ -19,7 +19,7 @@ import {
19
19
  import '@xyflow/react/dist/style.css';
20
20
  import { ExternalLink, HistoryIcon } from 'lucide-react';
21
21
  import { toPng } from 'html-to-image';
22
- import { DocumentArrowDownIcon } from '@heroicons/react/24/outline';
22
+ import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
23
23
  // Nodes and edges
24
24
  import ServiceNode from './Nodes/Service';
25
25
  import FlowNode from './Nodes/Flow';
@@ -135,6 +135,7 @@ const NodeGraphBuilder = ({
135
135
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
136
136
  const [animateMessages, setAnimateMessages] = useState(false);
137
137
  const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
138
+ const [isFullscreen, setIsFullscreen] = useState(false);
138
139
  // const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
139
140
 
140
141
  // Check if there are channels to determine if we need the visualizer functionality
@@ -349,6 +350,30 @@ const NodeGraphBuilder = ({
349
350
  setIsStudioModalOpen(true);
350
351
  };
351
352
 
353
+ const toggleFullScreen = useCallback(() => {
354
+ if (!document.fullscreenElement) {
355
+ reactFlowWrapperRef.current?.requestFullscreen().catch((err) => {
356
+ console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
357
+ });
358
+ } else {
359
+ document.exitFullscreen();
360
+ }
361
+ }, []);
362
+
363
+ useEffect(() => {
364
+ const handleFullscreenChange = () => {
365
+ setIsFullscreen(!!document.fullscreenElement);
366
+ setTimeout(() => {
367
+ fitView({ duration: 800 });
368
+ }, 100);
369
+ };
370
+
371
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
372
+ return () => {
373
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
374
+ };
375
+ }, [fitView]);
376
+
352
377
  const handleExportVisual = useCallback(() => {
353
378
  const imageWidth = 1024;
354
379
  const imageHeight = 768;
@@ -563,7 +588,7 @@ const NodeGraphBuilder = ({
563
588
  const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge');
564
589
 
565
590
  return (
566
- <div ref={reactFlowWrapperRef} className="w-full h-full">
591
+ <div ref={reactFlowWrapperRef} className="w-full h-full bg-gray-50">
567
592
  <ReactFlow
568
593
  nodeTypes={nodeTypes}
569
594
  edgeTypes={edgeTypes}
@@ -583,7 +608,7 @@ const NodeGraphBuilder = ({
583
608
  <Panel position="top-center" className="w-full pr-6 ">
584
609
  <div className="flex space-x-2 justify-between items-center">
585
610
  <div className="flex space-x-2 ml-4">
586
- <div>
611
+ <div className="relative group">
587
612
  <button
588
613
  onClick={() => setIsSettingsOpen(!isSettingsOpen)}
589
614
  className="py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
@@ -591,6 +616,23 @@ const NodeGraphBuilder = ({
591
616
  >
592
617
  <CogIcon className="h-5 w-5 text-gray-600" />
593
618
  </button>
619
+ <div className="absolute top-full left-0 mt-2 px-2 py-1 bg-gray-900 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
620
+ Settings
621
+ </div>
622
+ </div>
623
+ <div className="relative group">
624
+ <button
625
+ onClick={toggleFullScreen}
626
+ className={`py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 ${
627
+ isFullscreen ? 'bg-purple-50 text-purple-600' : ''
628
+ }`}
629
+ aria-label={isFullscreen ? 'Exit presentation mode' : 'Enter presentation mode'}
630
+ >
631
+ <PresentationChartLineIcon className={`h-5 w-5 ${isFullscreen ? 'text-purple-600' : 'text-gray-600'}`} />
632
+ </button>
633
+ <div className="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 bg-gray-900 text-white text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
634
+ {isFullscreen ? 'Exit Presentation Mode' : 'Presentation Mode'}
635
+ </div>
594
636
  </div>
595
637
 
596
638
  {title && (
@@ -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 },
@@ -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',
@@ -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
  };
@@ -786,17 +787,17 @@ export default function NestedSideBar() {
786
787
  </div>
787
788
  <div className="flex items-center gap-1 flex-shrink-0">
788
789
  {canFavorite && (
789
- <button
790
+ <div
790
791
  onClick={handleStarClick}
791
792
  className={cn(
792
- 'flex items-center justify-center w-5 h-5 rounded transition-colors',
793
+ 'flex items-center justify-center w-5 h-5 rounded transition-colors cursor-pointer',
793
794
  isFav
794
795
  ? 'text-amber-400 hover:text-amber-500'
795
796
  : 'text-gray-300 opacity-0 group-hover:opacity-100 hover:text-amber-400'
796
797
  )}
797
798
  >
798
799
  <Star className={cn('w-3.5 h-3.5', isFav && 'fill-current')} />
799
- </button>
800
+ </div>
800
801
  )}
801
802
  {itemHasChildren && (
802
803
  <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 +1039,15 @@ export default function NestedSideBar() {
1038
1039
  {fav.badge}
1039
1040
  </span>
1040
1041
  )}
1041
- <button
1042
+ <div
1042
1043
  onClick={(e) => {
1043
1044
  e.stopPropagation();
1044
1045
  if (node) toggleFavorite(fav.nodeKey, node);
1045
1046
  }}
1046
- className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors"
1047
+ className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1047
1048
  >
1048
1049
  <Star className="w-3.5 h-3.5 fill-current" />
1049
- </button>
1050
+ </div>
1050
1051
  {node?.pages && node.pages.length > 0 && (
1051
1052
  <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-black">
1052
1053
  <ChevronRight className="w-4 h-4" />
@@ -1060,7 +1061,20 @@ export default function NestedSideBar() {
1060
1061
  </div>
1061
1062
  )}
1062
1063
 
1063
- {renderEntries(currentLevel.entries)}
1064
+ {/* Empty State */}
1065
+ {currentLevel.entries.length === 0 && favorites.length === 0 && (
1066
+ <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1067
+ <div className="mb-4 p-3 rounded-full bg-gray-100">
1068
+ <FileQuestion className="w-8 h-8 text-gray-400" />
1069
+ </div>
1070
+ <h3 className="text-sm font-semibold text-gray-900 mb-2">Your catalog is empty</h3>
1071
+ <p className="text-xs text-gray-500 leading-relaxed max-w-[240px]">
1072
+ Navigation will appear here when you add resources to your EventCatalog.
1073
+ </p>
1074
+ </div>
1075
+ )}
1076
+
1077
+ {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1064
1078
  </nav>
1065
1079
  </>
1066
1080
  )}
@@ -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,
@@ -174,7 +174,7 @@ const topTiles = [
174
174
  description: 'Business domains defined',
175
175
  href: buildUrl('/discover/domains'),
176
176
  icon: RectangleGroupIcon,
177
- bgColor: 'bg-yellow-100',
177
+ bgColor: 'bg-yellow-500',
178
178
  textColor: 'text-yellow-600',
179
179
  arrowColor: 'text-yellow-600',
180
180
  },
@@ -184,7 +184,7 @@ const topTiles = [
184
184
  description: 'Services documented in the catalog',
185
185
  href: buildUrl('/discover/services'),
186
186
  icon: ServerIcon,
187
- bgColor: 'bg-pink-100',
187
+ bgColor: 'bg-pink-500',
188
188
  textColor: 'text-pink-600',
189
189
  arrowColor: 'text-pink-600',
190
190
  },
@@ -194,7 +194,7 @@ const topTiles = [
194
194
  description: 'Messages documented in the catalog',
195
195
  href: buildUrl('/discover/events'),
196
196
  icon: ChatBubbleLeftIcon,
197
- bgColor: 'bg-blue-100',
197
+ bgColor: 'bg-blue-500',
198
198
  textColor: 'text-blue-600',
199
199
  arrowColor: 'text-blue-600',
200
200
  },
@@ -204,7 +204,7 @@ const topTiles = [
204
204
  description: 'Business flows documented',
205
205
  href: buildUrl('/discover/flows'),
206
206
  icon: Workflow,
207
- bgColor: 'bg-purple-100',
207
+ bgColor: 'bg-purple-500',
208
208
  textColor: 'text-purple-600',
209
209
  arrowColor: 'text-purple-600',
210
210
  },
@@ -347,9 +347,7 @@ const quickActions = [
347
347
  <div class="absolute inset-0 bg-gradient-to-br from-gray-50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
348
348
 
349
349
  {/* Colored left border accent */}
350
- <div
351
- class={`absolute left-0 top-0 bottom-0 w-1 ${tile.bgColor} opacity-0 group-hover:opacity-100 transition-opacity duration-300`}
352
- />
350
+ <div class={`absolute left-0 top-0 bottom-0 w-1 ${tile.bgColor} transition-opacity duration-300`} />
353
351
 
354
352
  <div class="relative">
355
353
  <div class="flex items-center justify-between mb-4">
@@ -167,7 +167,7 @@ const providerConfig = {
167
167
 
168
168
  <div class="text-center">
169
169
  <a
170
- href="#"
170
+ href="https://www.eventcatalog.dev/docs/development/authentication/introduction"
171
171
  class="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors duration-200"
172
172
  >
173
173
  Read Authentication Documentation
@@ -237,7 +237,7 @@ const providerConfig = {
237
237
 
238
238
  <div class="text-center">
239
239
  <a
240
- href="#"
240
+ href="https://www.eventcatalog.dev/docs/development/authentication/introduction"
241
241
  class="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors duration-200"
242
242
  >
243
243
  Read Authentication Documentation
@@ -30,7 +30,33 @@ export function remarkDirectives() {
30
30
  class: `rounded-lg p-4 my-4 ${blockTypes[node.name as keyof typeof blockTypes] || ''}`,
31
31
  };
32
32
 
33
- // Create header div that will contain icon and type
33
+ // Check if there's a custom title (label) provided via :::note[Custom Title]
34
+ // In remark-directive, the label is stored in node.children as a paragraph node
35
+ // with data.directiveLabel = true
36
+ let titleChildren;
37
+ let contentChildren;
38
+
39
+ const firstChild = node.children && node.children.length > 0 ? node.children[0] : null;
40
+ const hasCustomTitle = firstChild && firstChild.data?.directiveLabel === true;
41
+
42
+ if (hasCustomTitle && firstChild) {
43
+ // Custom title was provided in the label - it contains markdown parsed as inline content
44
+ titleChildren = firstChild.children || [
45
+ { type: 'text', value: node.name.charAt(0).toUpperCase() + node.name.slice(1) },
46
+ ];
47
+ contentChildren = node.children.slice(1);
48
+ } else {
49
+ // No custom title, use default based on directive name
50
+ titleChildren = [
51
+ {
52
+ type: 'text',
53
+ value: node.name.charAt(0).toUpperCase() + node.name.slice(1),
54
+ },
55
+ ];
56
+ contentChildren = node.children;
57
+ }
58
+
59
+ // Create header div that will contain icon and title
34
60
  const headerNode = {
35
61
  type: 'element',
36
62
  data: {
@@ -70,7 +96,7 @@ export function remarkDirectives() {
70
96
  },
71
97
  ],
72
98
  },
73
- // Type label
99
+ // Title (with support for markdown)
74
100
  {
75
101
  type: 'element',
76
102
  data: {
@@ -79,12 +105,7 @@ export function remarkDirectives() {
79
105
  class: '',
80
106
  },
81
107
  },
82
- children: [
83
- {
84
- type: 'text',
85
- value: node.name.charAt(0).toUpperCase() + node.name.slice(1),
86
- },
87
- ],
108
+ children: titleChildren,
88
109
  },
89
110
  ],
90
111
  };
@@ -98,7 +119,7 @@ export function remarkDirectives() {
98
119
  class: 'prose prose-md w-full !max-w-none ',
99
120
  },
100
121
  },
101
- children: node.children,
122
+ children: contentChildren,
102
123
  };
103
124
 
104
125
  // Replace node's children with header and content
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "3.0.0-beta.1",
9
+ "version": "3.0.0-beta.11",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
@@ -87,10 +87,11 @@
87
87
  "nanostores": "^1.1.0",
88
88
  "pagefind": "^1.3.0",
89
89
  "pako": "^2.1.0",
90
+ "picocolors": "^1.1.1",
90
91
  "react": "^18.3.1",
91
92
  "react-dom": "^18.3.1",
92
93
  "react-markdown": "^10.1.0",
93
- "react-syntax-highlighter": "^15.6.1",
94
+ "react-syntax-highlighter": "^16.1.0",
94
95
  "rehype-autolink-headings": "^7.1.0",
95
96
  "rehype-expressive-code": "^0.41.3",
96
97
  "rehype-slug": "^6.0.0",