@eventcatalog/core 3.0.0-beta.0 → 3.0.0-beta.10

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 (47) 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-JB4YT5JY.js → chunk-37KBX24G.js} +1 -1
  6. package/dist/{chunk-3W6JYTHP.js → chunk-HIZ72XK6.js} +6 -2
  7. package/dist/{chunk-X4W4YC3U.js → chunk-QYFFI52L.js} +1 -1
  8. package/dist/{chunk-TQ4HZREX.js → chunk-VNKO4QEO.js} +1 -1
  9. package/dist/chunk-VY7TVWWV.js +44 -0
  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/Header.astro +5 -11
  22. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +45 -3
  23. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +0 -10
  24. package/eventcatalog/src/components/Search/Search.astro +2 -2
  25. package/eventcatalog/src/components/Search/SearchModal.tsx +16 -7
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +4 -0
  27. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +80 -52
  28. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +57 -11
  29. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +1 -1
  30. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +1 -1
  31. package/eventcatalog/src/layouts/DirectoryLayout.astro +1 -1
  32. package/eventcatalog/src/layouts/DiscoverLayout.astro +1 -1
  33. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +42 -155
  34. package/eventcatalog/src/pages/auth/login.astro +2 -2
  35. package/eventcatalog/src/pages/chat/feature.astro +1 -1
  36. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +4 -4
  37. package/eventcatalog/src/pages/docs/custom/feature.astro +1 -1
  38. package/eventcatalog/src/pages/docs/custom/index.astro +1 -1
  39. package/eventcatalog/src/pages/plans/index.astro +1 -1
  40. package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
  41. package/eventcatalog/src/pages/studio.astro +1 -1
  42. package/eventcatalog/src/pages/unauthorized/index.astro +1 -1
  43. package/eventcatalog/src/utils/collections/domains.ts +1 -1
  44. package/eventcatalog/src/utils/collections/types.ts +6 -0
  45. package/eventcatalog/src/utils/feature.ts +2 -0
  46. package/eventcatalog/tsconfig.json +2 -1
  47. package/package.json +3 -2
@@ -13,7 +13,7 @@ if (isAuthEnabled()) {
13
13
  }
14
14
 
15
15
  const logo = {
16
- src: ('/' + (catalog?.logo?.src || 'logo.png')).replace(/^\/+/, '/'),
16
+ src: ('/' + (catalog?.logo?.src || '')).replace(/^\/+/, '/'),
17
17
  alt: catalog?.logo?.alt || 'Event Catalog',
18
18
  text: catalog?.logo?.text || 'EventCatalog',
19
19
  };
@@ -30,7 +30,7 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
30
30
  <div class="flex-shrink-0 flex items-center w-3/12">
31
31
  <a href={buildUrl(catalog.landingPage || '/')} class="flex space-x-2 items-center group">
32
32
  {
33
- logo.src && (
33
+ logo.src && logo.src !== '/' && (
34
34
  <img alt={logo.alt} src={buildUrl(logo.src, true)} class="w-8 h-8 transition-transform group-hover:scale-105" />
35
35
  )
36
36
  }
@@ -42,7 +42,7 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
42
42
  </a>
43
43
  </div>
44
44
 
45
- <div class="hidden lg:block flex-grow w-6/12 px-10">
45
+ <div class="hidden lg:block flex-grow -ml-1">
46
46
  <Search />
47
47
  </div>
48
48
 
@@ -118,10 +118,7 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
118
118
  href="https://discord.com/invite/3rjaZMmrAm"
119
119
  class="block p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
120
120
  >
121
- <img
122
- src={buildUrl('/icons/discord.svg', true)}
123
- class="h-6 w-6 opacity-70 hover:opacity-100 transition-opacity"
124
- />
121
+ <img src={buildUrl('/icons/discord.svg', true)} class="h-6 w-6 hover:opacity-100 transition-opacity" />
125
122
  </a>
126
123
  </li>
127
124
  <li>
@@ -129,10 +126,7 @@ const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalo
129
126
  href="https://github.com/event-catalog/eventcatalog"
130
127
  class="block p-1.5 rounded-lg hover:bg-gray-100 transition-colors"
131
128
  >
132
- <img
133
- src={buildUrl('/icons/github.svg', true)}
134
- class="h-6 w-6 opacity-70 hover:opacity-100 transition-opacity"
135
- />
129
+ <img src={buildUrl('/icons/github.svg', true)} class="h-6 w-6 hover:opacity-100 transition-opacity" />
136
130
  </a>
137
131
  </li>
138
132
  </ul>
@@ -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 && (
@@ -314,16 +314,6 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
314
314
 
315
315
  return (
316
316
  <div className="h-full flex flex-col overflow-hidden">
317
- {/* Compact Header */}
318
- <div className="flex-shrink-0 border-b border-gray-200 pb-2 mb-3">
319
- <div>
320
- <h1 className="text-2xl font-bold text-gray-900">Schema Explorer</h1>
321
- <p className="mt-0.5 text-xs text-gray-600">
322
- {filteredMessages.length} schema{filteredMessages.length !== 1 ? 's' : ''} available
323
- </p>
324
- </div>
325
- </div>
326
-
327
317
  {/* Split View */}
328
318
  <div className="flex-1 flex gap-4 overflow-hidden">
329
319
  {/* Left: Filters + Schema List */}
@@ -4,7 +4,7 @@ import SearchModal from './SearchModal.tsx';
4
4
  ---
5
5
 
6
6
  <div>
7
- <div class="relative flex items-center">
7
+ <div class="relative flex items-center w-10/12">
8
8
  <input
9
9
  id="search-dummy-input"
10
10
  type="text"
@@ -13,7 +13,7 @@ import SearchModal from './SearchModal.tsx';
13
13
  autocomplete="off"
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
- <MagnifyingGlassIcon className="absolute inset-y-0 -left-1 h-9 w-8 flex items-center pl-4 text-gray-400" />
16
+ <MagnifyingGlassIcon className="absolute inset-y-0 left-0 h-9 w-8 flex items-center pl-4 text-gray-400" />
17
17
  <div class="absolute inset-y-0 right-0 flex py-1.5 pr-1.5">
18
18
  <kbd class="inline-flex items-center rounded px-1 font-sans text-xs text-gray-400">⌘K</kbd>
19
19
  </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 },
@@ -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
  };
@@ -42,13 +43,15 @@ type NavigationLevel = {
42
43
 
43
44
  export default function NestedSideBar() {
44
45
  const data = useStore(sidebarStore);
46
+ const favorites = useStore(favoritesStore);
45
47
 
46
48
  // Guard against undefined data (e.g., during hydration)
47
- const roots = data?.roots ?? [];
48
- const nodes = data?.nodes ?? {};
49
+ // Use useMemo to ensure stable references for roots and nodes
50
+ const roots = useMemo(() => data?.roots ?? [], [data?.roots]);
51
+ const nodes = useMemo(() => data?.nodes ?? {}, [data?.nodes]);
49
52
 
50
- const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>([
51
- { key: null, entries: roots, title: 'Documentation' },
53
+ const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(() => [
54
+ { key: null, entries: [], title: 'Documentation' },
52
55
  ]);
53
56
  const [animationKey, setAnimationKey] = useState(0);
54
57
  const [slideDirection, setSlideDirection] = useState<'forward' | 'backward' | null>(null);
@@ -57,7 +60,6 @@ export default function NestedSideBar() {
57
60
  const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set());
58
61
  const [showPathPreview, setShowPathPreview] = useState(false);
59
62
  const [showFullPath, setShowFullPath] = useState(false);
60
- const favorites = useStore(favoritesStore);
61
63
  const [isSearching, setIsSearching] = useState(false);
62
64
 
63
65
  // Build a lookup map for faster URL navigation
@@ -113,6 +115,21 @@ export default function NestedSideBar() {
113
115
  }
114
116
  }, []);
115
117
 
118
+ /**
119
+ * Update navigation stack when roots become available
120
+ */
121
+ useEffect(() => {
122
+ if (roots.length > 0) {
123
+ setNavigationStack((prevStack) => {
124
+ // Only update if the current stack has no entries (initial state)
125
+ if (prevStack.length === 1 && prevStack[0].entries.length === 0) {
126
+ return [{ key: null, entries: roots, title: 'Documentation' }];
127
+ }
128
+ return prevStack;
129
+ });
130
+ }
131
+ }, [roots]);
132
+
116
133
  /**
117
134
  * Populate the store with the data when the component mounts or data changes
118
135
  */
@@ -300,43 +317,47 @@ export default function NestedSideBar() {
300
317
  const foundNodeKey = findNodeKeyByUrl(url);
301
318
 
302
319
  if (foundNodeKey) {
303
- // Try to connect to current stack first
304
- const connectedStack = tryConnectStack(foundNodeKey, navigationStack);
320
+ setNavigationStack((currentStack) => {
321
+ // Try to connect to current stack first
322
+ const connectedStack = tryConnectStack(foundNodeKey, currentStack);
305
323
 
306
- if (connectedStack) {
307
- setNavigationStack(connectedStack);
308
- return true;
309
- }
324
+ if (connectedStack) {
325
+ return connectedStack;
326
+ }
310
327
 
311
- const foundNode = nodes[foundNodeKey];
312
- if (foundNode && foundNode.pages && foundNode.pages.length > 0) {
313
- // Fallback: Flattened navigation
314
- setNavigationStack([
315
- { key: null, entries: roots, title: 'Documentation' },
316
- { key: foundNodeKey, entries: foundNode.pages, title: foundNode.title, badge: foundNode.badge },
317
- ]);
318
- return true;
319
- }
328
+ const foundNode = nodes[foundNodeKey];
329
+ if (foundNode && foundNode.pages && foundNode.pages.length > 0) {
330
+ // Fallback: Flattened navigation
331
+ return [
332
+ { key: null, entries: roots, title: 'Documentation' },
333
+ { key: foundNodeKey, entries: foundNode.pages, title: foundNode.title, badge: foundNode.badge },
334
+ ];
335
+ }
336
+
337
+ return currentStack;
338
+ });
339
+ return true;
320
340
  } else if (url === '/' || url === '') {
321
341
  // Reset to root if we are on homepage
322
- if (navigationStack.length > 1) {
323
- setSlideDirection('backward');
324
- setAnimationKey((prev) => prev + 1);
325
- }
326
- setNavigationStack([{ key: null, entries: roots, title: 'Documentation' }]);
342
+ setNavigationStack((currentStack) => {
343
+ if (currentStack.length > 1) {
344
+ setSlideDirection('backward');
345
+ setAnimationKey((prev) => prev + 1);
346
+ }
347
+ return [{ key: null, entries: roots, title: 'Documentation' }];
348
+ });
327
349
  return true;
328
350
  }
329
351
  return false;
330
352
  },
331
- [findNodeKeyByUrl, tryConnectStack, navigationStack, nodes, roots]
353
+ [findNodeKeyByUrl, tryConnectStack, nodes, roots]
332
354
  );
333
355
 
334
356
  /**
335
357
  * Restore state from localStorage on mount, or navigate to URL
336
358
  */
337
359
  useEffect(() => {
338
- if (!data || roots.length === 0) return;
339
- if (isInitialized) return;
360
+ if (!data || roots.length === 0 || isInitialized) return;
340
361
 
341
362
  const currentUrl = window.location.pathname;
342
363
 
@@ -387,7 +408,7 @@ export default function NestedSideBar() {
387
408
  }
388
409
 
389
410
  setIsInitialized(true);
390
- }, [data, roots, buildStackFromPath, isInitialized, findNodeKeyByUrl, tryConnectStack, nodes]);
411
+ }, [data, roots, nodes, isInitialized, buildStackFromPath, findNodeKeyByUrl, tryConnectStack]);
391
412
 
392
413
  /**
393
414
  * Save state whenever navigation changes
@@ -750,10 +771,7 @@ export default function NestedSideBar() {
750
771
  <div className="flex items-center gap-2.5 min-w-0 flex-1 ">
751
772
  {IconComponent && (
752
773
  <span
753
- className={cn(
754
- 'flex items-center justify-center w-5 h-5 flex-shrink-0',
755
- isActive ? 'text-purple-600' : 'text-gray-500'
756
- )}
774
+ className={cn('flex items-center justify-center w-5 h-5 flex-shrink-0', isActive ? 'text-black' : 'text-gray-500')}
757
775
  >
758
776
  <IconComponent className="w-4 h-4" />
759
777
  </span>
@@ -761,7 +779,7 @@ export default function NestedSideBar() {
761
779
  <span
762
780
  className={cn(
763
781
  'text-[13px] truncate',
764
- isActive ? 'text-purple-600 font-medium' : 'text-gray-600 group-hover:text-gray-900'
782
+ isActive ? 'text-black font-medium' : 'text-gray-600 group-hover:text-gray-900'
765
783
  )}
766
784
  >
767
785
  {item.title}
@@ -769,20 +787,20 @@ export default function NestedSideBar() {
769
787
  </div>
770
788
  <div className="flex items-center gap-1 flex-shrink-0">
771
789
  {canFavorite && (
772
- <button
790
+ <div
773
791
  onClick={handleStarClick}
774
792
  className={cn(
775
- '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',
776
794
  isFav
777
795
  ? 'text-amber-400 hover:text-amber-500'
778
796
  : 'text-gray-300 opacity-0 group-hover:opacity-100 hover:text-amber-400'
779
797
  )}
780
798
  >
781
799
  <Star className={cn('w-3.5 h-3.5', isFav && 'fill-current')} />
782
- </button>
800
+ </div>
783
801
  )}
784
802
  {itemHasChildren && (
785
- <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-purple-500 group-hover:translate-x-0.5 transition-transform">
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">
786
804
  <ChevronRight className="w-4 h-4" />
787
805
  </span>
788
806
  )}
@@ -793,7 +811,7 @@ export default function NestedSideBar() {
793
811
  const baseClasses =
794
812
  'group flex items-center justify-between w-full px-3 py-1 rounded-lg cursor-pointer text-left transition-colors hover:bg-gray-100 active:bg-gray-200';
795
813
  const parentClasses = itemHasChildren ? 'font-medium' : '';
796
- const activeClasses = isActive ? 'bg-purple-100 hover:bg-purple-100 border-l-4 border-purple-600 rounded-l-none' : '';
814
+ const activeClasses = isActive ? 'bg-gray-200 hover:bg-gray-200 border-l-4 border-black rounded-l-none' : '';
797
815
 
798
816
  // Leaf item with href → render as link
799
817
  if (item.href && !itemHasChildren) {
@@ -899,7 +917,7 @@ export default function NestedSideBar() {
899
917
  className={cn(
900
918
  'flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors',
901
919
  !isCurrentLevel && 'hover:bg-gray-100 cursor-pointer',
902
- isCurrentLevel && 'bg-purple-50 cursor-default'
920
+ isCurrentLevel && 'bg-gray-200 cursor-default'
903
921
  )}
904
922
  style={{ paddingLeft: `${displayIndex * 12 + 8}px` }}
905
923
  >
@@ -909,10 +927,7 @@ export default function NestedSideBar() {
909
927
  <ChevronRight className="w-3.5 h-3.5 text-gray-300 flex-shrink-0" />
910
928
  )}
911
929
  <span
912
- className={cn(
913
- 'text-sm truncate',
914
- isCurrentLevel ? 'font-medium text-purple-700' : 'text-gray-600'
915
- )}
930
+ className={cn('text-sm truncate', isCurrentLevel ? 'font-medium text-black' : 'text-gray-600')}
916
931
  >
917
932
  {level.title}
918
933
  </span>
@@ -1000,14 +1015,14 @@ export default function NestedSideBar() {
1000
1015
  onClick={() => navigateToFavorite(fav)}
1001
1016
  className={cn(
1002
1017
  'group flex items-center justify-between w-full px-3 py-1.5 rounded-lg cursor-pointer text-left transition-colors hover:bg-amber-50 active:bg-amber-100',
1003
- isActive && 'bg-purple-100 hover:bg-purple-100 border-l-4 border-purple-600 rounded-l-none'
1018
+ isActive && 'bg-gray-200 hover:bg-gray-200 border-l-4 border-black rounded-l-none'
1004
1019
  )}
1005
1020
  >
1006
1021
  <div className="flex items-center gap-2.5 min-w-0 flex-1">
1007
1022
  <span
1008
1023
  className={cn(
1009
1024
  'text-[14px] truncate',
1010
- isActive ? 'text-purple-600 font-medium' : 'text-gray-600 group-hover:text-gray-900'
1025
+ isActive ? 'text-black font-medium' : 'text-gray-600 group-hover:text-gray-900'
1011
1026
  )}
1012
1027
  >
1013
1028
  {fav.title}
@@ -1024,17 +1039,17 @@ export default function NestedSideBar() {
1024
1039
  {fav.badge}
1025
1040
  </span>
1026
1041
  )}
1027
- <button
1042
+ <div
1028
1043
  onClick={(e) => {
1029
1044
  e.stopPropagation();
1030
1045
  if (node) toggleFavorite(fav.nodeKey, node);
1031
1046
  }}
1032
- 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"
1033
1048
  >
1034
1049
  <Star className="w-3.5 h-3.5 fill-current" />
1035
- </button>
1050
+ </div>
1036
1051
  {node?.pages && node.pages.length > 0 && (
1037
- <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-purple-500">
1052
+ <span className="flex items-center justify-center w-5 h-5 text-gray-400 group-hover:text-black">
1038
1053
  <ChevronRight className="w-4 h-4" />
1039
1054
  </span>
1040
1055
  )}
@@ -1046,7 +1061,20 @@ export default function NestedSideBar() {
1046
1061
  </div>
1047
1062
  )}
1048
1063
 
1049
- {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)}
1050
1078
  </nav>
1051
1079
  </>
1052
1080
  )}