@eventcatalog/core 3.29.2 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) 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-36IA4UE4.js → chunk-6UG4JMUV.js} +1 -1
  6. package/dist/{chunk-MEJOYC5Z.js → chunk-ATRBVTJ6.js} +1 -1
  7. package/dist/{chunk-VEUNSJ6Z.js → chunk-MVZKHUX2.js} +1 -1
  8. package/dist/{chunk-DB4IQ3GB.js → chunk-RRBDF4MM.js} +1 -1
  9. package/dist/{chunk-EGQGCB2B.js → chunk-Z26P4PCB.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/astro.config.mjs +1 -1
  19. package/eventcatalog/public/logo.png +0 -0
  20. package/eventcatalog/src/components/CopyAsMarkdown.tsx +2 -2
  21. package/eventcatalog/src/components/EnvironmentDropdown.tsx +33 -21
  22. package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +3 -53
  23. package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +144 -91
  24. package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +112 -109
  25. package/eventcatalog/src/components/Header.astro +9 -19
  26. package/eventcatalog/src/components/MDX/Accordion/Accordion.tsx +12 -14
  27. package/eventcatalog/src/components/MDX/Accordion/AccordionGroup.astro +11 -3
  28. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +15 -5
  29. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +164 -53
  30. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +1 -1
  31. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +4 -4
  32. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +12 -10
  33. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +48 -77
  34. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +238 -169
  35. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +189 -230
  36. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +39 -36
  37. package/eventcatalog/src/components/Search/Search.astro +1 -1
  38. package/eventcatalog/src/components/Seo.astro +1 -1
  39. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +3 -3
  40. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +229 -256
  41. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +78 -59
  42. package/eventcatalog/src/components/Tables/Discover/columns.tsx +130 -197
  43. package/eventcatalog/src/components/Tables/Table.tsx +21 -18
  44. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +79 -131
  45. package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +104 -175
  46. package/eventcatalog/src/enterprise/auth/error.astro +1 -1
  47. package/eventcatalog/src/enterprise/auth/login.astro +1 -1
  48. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +95 -93
  49. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +174 -136
  50. package/eventcatalog/src/enterprise/fields/pages/fields.astro +10 -8
  51. package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +0 -8
  52. package/eventcatalog/src/layouts/DirectoryLayout.astro +17 -88
  53. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +528 -146
  54. package/eventcatalog/src/layouts/VisualiserLayout.astro +7 -2
  55. package/eventcatalog/src/pages/_index.astro +5 -3
  56. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -3
  57. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +223 -73
  58. package/eventcatalog/src/pages/discover/[type]/index.astro +22 -141
  59. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +129 -29
  60. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +129 -29
  61. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +6 -2
  62. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +2 -2
  63. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +21 -18
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +33 -32
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +5 -1
  66. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +2 -2
  67. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +4 -6
  68. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +11 -4
  69. package/eventcatalog/src/pages/docs/users/[id]/index.astro +11 -4
  70. package/eventcatalog/src/pages/schemas/explorer/index.astro +10 -8
  71. package/eventcatalog/src/pages/studio.astro +1 -1
  72. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +2 -7
  73. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +2 -2
  74. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +2 -7
  75. package/eventcatalog/src/styles/theme.css +68 -12
  76. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +13 -0
  77. package/package.json +1 -1
  78. package/eventcatalog/public/logo.svg +0 -14
  79. package/eventcatalog/src/enterprise/plans/index.astro +0 -319
@@ -4,7 +4,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
4
4
  import * as LucideIcons from 'lucide-react';
5
5
  import { ChevronRight, ChevronLeft, ChevronDown, Home, Star, FileQuestion } from 'lucide-react';
6
6
  import type { NavNode, ChildRef } from '@stores/sidebar-store/state';
7
- import SearchBar from './SearchBar';
8
7
  import { saveState, loadState, saveCollapsedSections, loadCollapsedSections } from './storage';
9
8
  import { useStore } from '@nanostores/react';
10
9
  import { sidebarStore } from '@stores/sidebar-store';
@@ -61,7 +60,6 @@ export default function NestedSideBar() {
61
60
  const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set());
62
61
  const [showPathPreview, setShowPathPreview] = useState(false);
63
62
  const [showFullPath, setShowFullPath] = useState(false);
64
- const [isSearching, setIsSearching] = useState(false);
65
63
 
66
64
  // Build a lookup map for faster URL navigation
67
65
  // Map format: "type:id" -> "nodeKey"
@@ -503,13 +501,13 @@ export default function NestedSideBar() {
503
501
  // Show loading state if no data yet
504
502
  if (!data || roots.length === 0) {
505
503
  return (
506
- <aside className="w-[315px] h-full flex flex-col font-sans">
504
+ <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-page-bg))]">
507
505
  {/* Search skeleton */}
508
- <div className="px-3 py-3 border-b border-[rgb(var(--ec-content-border))]">
509
- <div className="h-9 bg-[rgb(var(--ec-content-hover))] rounded-lg animate-pulse" />
506
+ <div className="px-4 py-3 border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-page-bg))]">
507
+ <div className="h-10 bg-[rgb(var(--ec-content-hover))] rounded-xl animate-pulse" />
510
508
  </div>
511
509
  {/* Content skeleton */}
512
- <div className="p-3 space-y-3">
510
+ <div className="p-4 space-y-3">
513
511
  {/* Group header skeleton */}
514
512
  <div className="flex items-center gap-2 px-2 py-1.5">
515
513
  <div className="w-3.5 h-3.5 bg-[rgb(var(--ec-content-hover))] rounded animate-pulse" />
@@ -665,32 +663,6 @@ export default function NestedSideBar() {
665
663
 
666
664
  const isTopLevel = navigationStack.length === 1;
667
665
 
668
- /**
669
- * Navigate to a search result
670
- */
671
- const navigateToSearchResult = (nodeKey: string, node: NavNode) => {
672
- // If it's a leaf node with href, navigate directly
673
- if (node.href && (!node.pages || node.pages.length === 0)) {
674
- window.location.href = node.href;
675
- return;
676
- }
677
-
678
- // If it has children, drill down to it
679
- if (node.pages && node.pages.length > 0) {
680
- setSlideDirection('forward');
681
- setAnimationKey((prev) => prev + 1);
682
- setNavigationStack([
683
- { key: null, entries: roots, title: 'Documentation' },
684
- { key: nodeKey, entries: node.pages, title: node.title, badge: node.badge },
685
- ]);
686
- }
687
-
688
- setIsSearching(false);
689
- // Reset hover states
690
- setShowPathPreview(false);
691
- setShowFullPath(false);
692
- };
693
-
694
666
  /**
695
667
  * Render a list of child refs (resolving keys as needed)
696
668
  */
@@ -779,8 +751,8 @@ export default function NestedSideBar() {
779
751
  className={cn(
780
752
  'tracking-tight',
781
753
  isSubtleGroup
782
- ? 'text-[12px] text-[rgb(var(--ec-content-text-muted))] font-medium'
783
- : 'text-[13px] text-[rgb(var(--ec-content-text))] font-semibold'
754
+ ? 'text-[11px] text-[rgb(var(--ec-content-text-muted))] font-medium'
755
+ : 'text-[12px] text-[rgb(var(--ec-content-text))] font-semibold'
784
756
  )}
785
757
  >
786
758
  {group.title}
@@ -891,7 +863,7 @@ export default function NestedSideBar() {
891
863
  {item.leftIcon && <img src={resolveIconUrl(item.leftIcon)} alt="" loading="lazy" className="w-4 h-4 flex-shrink-0" />}
892
864
  <span
893
865
  className={cn(
894
- 'text-[13px] truncate',
866
+ 'text-[12px] truncate',
895
867
  isActive
896
868
  ? 'text-[rgb(var(--ec-accent-text))] font-medium'
897
869
  : 'text-[rgb(var(--ec-content-text-secondary))] group-hover:text-[rgb(var(--ec-content-text))]'
@@ -924,10 +896,10 @@ export default function NestedSideBar() {
924
896
  );
925
897
 
926
898
  const baseClasses =
927
- 'group flex items-center justify-between w-full px-3 py-1.5 rounded-lg cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] active:bg-[rgb(var(--ec-content-hover))]';
899
+ 'group flex items-center justify-between w-full px-3 py-2 border border-transparent cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] active:bg-[rgb(var(--ec-content-hover))]';
928
900
  const parentClasses = itemHasChildren ? 'font-medium' : '';
929
901
  const activeClasses = isActive
930
- ? 'bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] rounded-none!'
902
+ ? 'border-[rgb(var(--ec-accent)/0.16)] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] shadow-sm'
931
903
  : '';
932
904
 
933
905
  // Leaf item with href → render as link
@@ -966,251 +938,252 @@ export default function NestedSideBar() {
966
938
  };
967
939
 
968
940
  return (
969
- <aside className="w-[315px] h-full flex flex-col font-sans">
970
- {/* Search */}
971
- <SearchBar nodes={nodes} onSelectResult={navigateToSearchResult} onSearchChange={setIsSearching} />
972
-
973
- {/* Back Navigation and Nav Content - hidden when showing search results */}
974
- {!isSearching && (
975
- <>
976
- {!isTopLevel && (
977
- <div
978
- className="px-3 py-2 bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10"
979
- onMouseEnter={() => !isTopLevel && setShowPathPreview(true)}
980
- onMouseLeave={() => {
981
- setShowPathPreview(false);
982
- setShowFullPath(false);
983
- }}
941
+ <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-page-bg))]">
942
+ {isTopLevel && (
943
+ <div className="px-4 py-[13.5px] bg-[rgb(var(--ec-page-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
944
+ <div className="flex items-center w-full px-2 py-1.5">
945
+ <span className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] truncate">All resources</span>
946
+ </div>
947
+ </div>
948
+ )}
949
+
950
+ {!isTopLevel && (
951
+ <div
952
+ className="px-4 py-[13.5px] bg-[rgb(var(--ec-page-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10"
953
+ onMouseEnter={() => !isTopLevel && setShowPathPreview(true)}
954
+ onMouseLeave={() => {
955
+ setShowPathPreview(false);
956
+ setShowFullPath(false);
957
+ }}
958
+ >
959
+ <button
960
+ onClick={navigateBack}
961
+ disabled={isTopLevel}
962
+ className={cn(
963
+ 'flex items-center gap-2 w-full px-2 py-1.5 -mx-2 rounded-md transition-colors',
964
+ !isTopLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
965
+ isTopLevel && 'cursor-default'
966
+ )}
967
+ >
968
+ <span
969
+ className={cn(
970
+ 'flex items-center justify-center w-5 h-5 text-[rgb(var(--ec-icon-color))] transition-all',
971
+ isTopLevel && 'opacity-0',
972
+ !isTopLevel && 'group-hover:-translate-x-0.5'
973
+ )}
984
974
  >
985
- <button
986
- onClick={navigateBack}
987
- disabled={isTopLevel}
975
+ <ChevronLeft className="w-4 h-4" />
976
+ </span>
977
+ <span className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] truncate">{currentLevel.title}</span>
978
+ {currentLevel.badge && (
979
+ <span
988
980
  className={cn(
989
- 'flex items-center gap-2 w-full px-2 py-1.5 -mx-2 rounded-md transition-colors',
990
- !isTopLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
991
- isTopLevel && 'cursor-default'
981
+ 'ml-auto px-2 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
982
+ getBadgeClasses(currentLevel.badge)
992
983
  )}
993
984
  >
994
- <span
995
- className={cn(
996
- 'flex items-center justify-center w-5 h-5 text-[rgb(var(--ec-icon-color))] transition-all',
997
- isTopLevel && 'opacity-0',
998
- !isTopLevel && 'group-hover:-translate-x-0.5'
999
- )}
1000
- >
1001
- <ChevronLeft className="w-4 h-4" />
1002
- </span>
1003
- <span className="text-sm font-semibold text-[rgb(var(--ec-content-text))] truncate">{currentLevel.title}</span>
1004
- {currentLevel.badge && (
1005
- <span
1006
- className={cn(
1007
- 'ml-auto px-2 py-0.5 text-[9px] font-semibold uppercase tracking-wide rounded',
1008
- getBadgeClasses(currentLevel.badge)
1009
- )}
1010
- >
1011
- {currentLevel.badge}
1012
- </span>
1013
- )}
1014
- </button>
1015
-
1016
- {/* Path Preview Dropdown */}
1017
- {showPathPreview && navigationStack.length > 1 && (
1018
- <div className="absolute left-0 right-0 top-full bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))] shadow-lg z-20">
1019
- <div className="px-3 py-2">
1020
- <div className="text-[10px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide mb-2">
1021
- Navigation Path
1022
- </div>
1023
- <div className="flex flex-col gap-0.5">
1024
- {(() => {
1025
- const SHOW_FIRST = 2; // Show first N items
1026
- const SHOW_LAST = 2; // Show last N items (including current)
1027
- const totalItems = navigationStack.length;
1028
- const hiddenCount = totalItems - SHOW_FIRST - SHOW_LAST;
1029
- const shouldTruncate = hiddenCount > 0 && !showFullPath;
1030
-
1031
- const renderPathItem = (level: NavigationLevel, index: number, displayIndex: number) => {
1032
- const isCurrentLevel = index === navigationStack.length - 1;
1033
- return (
1034
- <button
1035
- key={`path-${index}`}
1036
- onClick={() => navigateToLevel(index)}
1037
- disabled={isCurrentLevel}
1038
- className={cn(
1039
- 'flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors',
1040
- !isCurrentLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
1041
- isCurrentLevel && 'bg-[rgb(var(--ec-content-hover))] cursor-default'
1042
- )}
1043
- style={{ paddingLeft: `${displayIndex * 12 + 8}px` }}
1044
- >
1045
- {index === 0 ? (
1046
- <Home className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] flex-shrink-0" />
1047
- ) : (
1048
- <ChevronRight className="w-3.5 h-3.5 text-[rgb(var(--ec-content-text-muted))] flex-shrink-0" />
1049
- )}
1050
- <span
1051
- className={cn(
1052
- 'text-sm truncate',
1053
- isCurrentLevel
1054
- ? 'font-medium text-[rgb(var(--ec-content-text))]'
1055
- : 'text-[rgb(var(--ec-content-text-secondary))]'
1056
- )}
1057
- >
1058
- {level.title}
1059
- </span>
1060
- {level.badge && (
1061
- <span
1062
- className={cn(
1063
- 'ml-auto px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded flex-shrink-0',
1064
- getBadgeClasses(level.badge)
1065
- )}
1066
- >
1067
- {level.badge}
1068
- </span>
1069
- )}
1070
- </button>
1071
- );
1072
- };
1073
-
1074
- if (shouldTruncate) {
1075
- return (
1076
- <>
1077
- {/* First N items */}
1078
- {navigationStack.slice(0, SHOW_FIRST).map((level, index) => renderPathItem(level, index, index))}
1079
-
1080
- {/* Collapsed middle section */}
1081
- <button
1082
- onClick={(e) => {
1083
- e.stopPropagation();
1084
- setShowFullPath(true);
1085
- }}
1086
- className="flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer"
1087
- style={{ paddingLeft: `${SHOW_FIRST * 12 + 8}px` }}
1088
- >
1089
- <span className="flex items-center justify-center w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))]">
1090
- <span className="text-xs">•••</span>
1091
- </span>
1092
- <span className="text-sm text-[rgb(var(--ec-content-text-muted))]">
1093
- {hiddenCount} more level{hiddenCount > 1 ? 's' : ''}
1094
- </span>
1095
- <ChevronDown className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] ml-auto" />
1096
- </button>
1097
-
1098
- {/* Last N items */}
1099
- {navigationStack.slice(-SHOW_LAST).map((level, sliceIndex) => {
1100
- const actualIndex = totalItems - SHOW_LAST + sliceIndex;
1101
- return renderPathItem(level, actualIndex, SHOW_FIRST + 1 + sliceIndex);
1102
- })}
1103
- </>
1104
- );
1105
- }
1106
-
1107
- // Show full path
1108
- return navigationStack.map((level, index) => renderPathItem(level, index, index));
1109
- })()}
1110
- </div>
1111
- </div>
1112
- </div>
1113
- )}
1114
- </div>
1115
- )}
985
+ {currentLevel.badge}
986
+ </span>
987
+ )}
988
+ </button>
1116
989
 
1117
- {/* Navigation Content */}
1118
- <nav
1119
- key={animationKey}
1120
- className={cn('flex-1 overflow-y-auto overflow-x-hidden p-3', getAnimationClass())}
1121
- style={{
1122
- scrollbarWidth: 'thin',
1123
- scrollbarColor: 'rgb(var(--ec-content-border)) transparent',
1124
- }}
1125
- >
1126
- {/* Favorites Section */}
1127
- {favorites.length > 0 && isTopLevel && (
1128
- <div className="mb-6">
1129
- <div className="flex items-center px-2 py-1.5">
1130
- <Star className="w-3.5 h-3.5 mr-2 text-amber-400 fill-current" />
1131
- <span className="text-sm text-[rgb(var(--ec-content-text))] font-semibold">Favorites</span>
990
+ {/* Path Preview Dropdown */}
991
+ {showPathPreview && navigationStack.length > 1 && (
992
+ <div className="absolute left-0 right-0 top-full bg-[rgb(var(--ec-page-bg))] border-b border-[rgb(var(--ec-content-border))] shadow-lg z-20">
993
+ <div className="px-4 py-3">
994
+ <div className="text-[9px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide mb-2">
995
+ Navigation Path
1132
996
  </div>
1133
- <div className="flex flex-col gap-0.5 border-l ml-3.5 border-amber-200">
1134
- {favorites.map((fav, index) => {
1135
- const node = resolveRef(fav.nodeKey);
1136
- const isActive = fav.href && currentPath === fav.href;
1137
-
1138
- return (
1139
- <button
1140
- key={`fav-${index}`}
1141
- onClick={() => navigateToFavorite(fav)}
1142
- className={cn(
1143
- 'group flex items-center justify-between w-full px-3 py-1.5 rounded-lg cursor-pointer text-left transition-colors hover:bg-amber-500/10 active:bg-amber-500/20',
1144
- isActive && 'bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] rounded-none!'
1145
- )}
1146
- >
1147
- <div className="flex items-center gap-2.5 min-w-0 flex-1">
997
+ <div className="flex flex-col gap-0.5">
998
+ {(() => {
999
+ const SHOW_FIRST = 2; // Show first N items
1000
+ const SHOW_LAST = 2; // Show last N items (including current)
1001
+ const totalItems = navigationStack.length;
1002
+ const hiddenCount = totalItems - SHOW_FIRST - SHOW_LAST;
1003
+ const shouldTruncate = hiddenCount > 0 && !showFullPath;
1004
+
1005
+ const renderPathItem = (level: NavigationLevel, index: number, displayIndex: number) => {
1006
+ const isCurrentLevel = index === navigationStack.length - 1;
1007
+ return (
1008
+ <button
1009
+ key={`path-${index}`}
1010
+ onClick={() => navigateToLevel(index)}
1011
+ disabled={isCurrentLevel}
1012
+ className={cn(
1013
+ 'flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors',
1014
+ !isCurrentLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
1015
+ isCurrentLevel && 'bg-[rgb(var(--ec-content-hover))] cursor-default'
1016
+ )}
1017
+ style={{ paddingLeft: `${displayIndex * 12 + 8}px` }}
1018
+ >
1019
+ {index === 0 ? (
1020
+ <Home className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] flex-shrink-0" />
1021
+ ) : (
1022
+ <ChevronRight className="w-3.5 h-3.5 text-[rgb(var(--ec-content-text-muted))] flex-shrink-0" />
1023
+ )}
1148
1024
  <span
1149
1025
  className={cn(
1150
- 'text-[14px] truncate',
1151
- isActive
1152
- ? 'text-[rgb(var(--ec-accent-text))] font-medium'
1153
- : 'text-[rgb(var(--ec-content-text-secondary))] group-hover:text-[rgb(var(--ec-content-text))]'
1026
+ 'text-[12px] truncate',
1027
+ isCurrentLevel
1028
+ ? 'font-medium text-[rgb(var(--ec-content-text))]'
1029
+ : 'text-[rgb(var(--ec-content-text-secondary))]'
1154
1030
  )}
1155
1031
  >
1156
- {fav.title}
1032
+ {level.title}
1157
1033
  </span>
1158
- </div>
1159
- <div className="flex items-center gap-1 flex-shrink-0">
1160
- {fav.badge && (
1034
+ {level.badge && (
1161
1035
  <span
1162
1036
  className={cn(
1163
- 'px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
1164
- getBadgeClasses(fav.badge)
1037
+ 'ml-auto px-1.5 py-0.5 text-[7px] font-semibold uppercase tracking-wide rounded flex-shrink-0',
1038
+ getBadgeClasses(level.badge)
1165
1039
  )}
1166
1040
  >
1167
- {fav.badge}
1041
+ {level.badge}
1168
1042
  </span>
1169
1043
  )}
1170
- <div
1044
+ </button>
1045
+ );
1046
+ };
1047
+
1048
+ if (shouldTruncate) {
1049
+ return (
1050
+ <>
1051
+ {/* First N items */}
1052
+ {navigationStack.slice(0, SHOW_FIRST).map((level, index) => renderPathItem(level, index, index))}
1053
+
1054
+ {/* Collapsed middle section */}
1055
+ <button
1171
1056
  onClick={(e) => {
1172
1057
  e.stopPropagation();
1173
- if (node) {
1174
- toggleFavorite(fav.nodeKey, node);
1175
- } else {
1176
- // Node no longer exists, remove directly using nodeKey
1177
- removeFavoriteAction(fav.nodeKey);
1178
- }
1058
+ setShowFullPath(true);
1179
1059
  }}
1180
- className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1060
+ className="flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer"
1061
+ style={{ paddingLeft: `${SHOW_FIRST * 12 + 8}px` }}
1181
1062
  >
1182
- <Star className="w-3.5 h-3.5 fill-current" />
1183
- </div>
1184
- {node?.pages && node.pages.length > 0 && (
1185
- <span className="flex items-center justify-center w-5 h-5 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-content-text))]">
1186
- <ChevronRight className="w-4 h-4" />
1063
+ <span className="flex items-center justify-center w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))]">
1064
+ <span className="text-xs">•••</span>
1187
1065
  </span>
1188
- )}
1189
- </div>
1190
- </button>
1191
- );
1192
- })}
1066
+ <span className="text-[12px] text-[rgb(var(--ec-content-text-muted))]">
1067
+ {hiddenCount} more level{hiddenCount > 1 ? 's' : ''}
1068
+ </span>
1069
+ <ChevronDown className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] ml-auto" />
1070
+ </button>
1071
+
1072
+ {/* Last N items */}
1073
+ {navigationStack.slice(-SHOW_LAST).map((level, sliceIndex) => {
1074
+ const actualIndex = totalItems - SHOW_LAST + sliceIndex;
1075
+ return renderPathItem(level, actualIndex, SHOW_FIRST + 1 + sliceIndex);
1076
+ })}
1077
+ </>
1078
+ );
1079
+ }
1080
+
1081
+ // Show full path
1082
+ return navigationStack.map((level, index) => renderPathItem(level, index, index));
1083
+ })()}
1193
1084
  </div>
1194
1085
  </div>
1195
- )}
1086
+ </div>
1087
+ )}
1088
+ </div>
1089
+ )}
1196
1090
 
1197
- {/* Empty State */}
1198
- {currentLevel.entries.length === 0 && favorites.length === 0 && (
1199
- <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1200
- <div className="mb-4 p-3 rounded-full bg-[rgb(var(--ec-group-icon-bg))]">
1201
- <FileQuestion className="w-8 h-8 text-[rgb(var(--ec-icon-color))]" />
1202
- </div>
1203
- <h3 className="text-sm font-semibold text-[rgb(var(--ec-content-text))] mb-2">Your catalog is empty</h3>
1204
- <p className="text-xs text-[rgb(var(--ec-content-text-muted))] leading-relaxed max-w-[240px]">
1205
- Navigation will appear here when you add resources to your EventCatalog.
1206
- </p>
1207
- </div>
1208
- )}
1091
+ {/* Navigation Content */}
1092
+ <nav
1093
+ key={animationKey}
1094
+ className={cn('flex-1 overflow-y-auto overflow-x-hidden p-4 px-2', getAnimationClass())}
1095
+ style={{
1096
+ scrollbarWidth: 'thin',
1097
+ scrollbarColor: 'rgb(var(--ec-content-border)) transparent',
1098
+ }}
1099
+ >
1100
+ {/* Favorites Section */}
1101
+ {favorites.length > 0 && isTopLevel && (
1102
+ <div className="mb-6">
1103
+ <div className="flex items-center px-2 py-1.5">
1104
+ <Star className="w-3.5 h-3.5 mr-2 text-amber-400 fill-current" />
1105
+ <span className="text-[12px] text-[rgb(var(--ec-content-text))] font-semibold">Favorites</span>
1106
+ </div>
1107
+ <div className="flex flex-col gap-0.5 border-l ml-3.5 border-amber-200">
1108
+ {favorites.map((fav, index) => {
1109
+ const node = resolveRef(fav.nodeKey);
1110
+ const isActive = fav.href && currentPath === fav.href;
1209
1111
 
1210
- {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1211
- </nav>
1212
- </>
1213
- )}
1112
+ return (
1113
+ <button
1114
+ key={`fav-${index}`}
1115
+ onClick={() => navigateToFavorite(fav)}
1116
+ className={cn(
1117
+ 'group flex items-center justify-between w-full px-3 py-2 border border-transparent cursor-pointer text-left transition-colors hover:bg-amber-500/10 active:bg-amber-500/20',
1118
+ isActive &&
1119
+ 'border-[rgb(var(--ec-accent)/0.16)] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] shadow-sm'
1120
+ )}
1121
+ >
1122
+ <div className="flex items-center gap-2.5 min-w-0 flex-1">
1123
+ <span
1124
+ className={cn(
1125
+ 'text-[12px] truncate',
1126
+ isActive
1127
+ ? 'text-[rgb(var(--ec-accent-text))] font-medium'
1128
+ : 'text-[rgb(var(--ec-content-text-secondary))] group-hover:text-[rgb(var(--ec-content-text))]'
1129
+ )}
1130
+ >
1131
+ {fav.title}
1132
+ </span>
1133
+ </div>
1134
+ <div className="flex items-center gap-1 flex-shrink-0">
1135
+ {fav.badge && (
1136
+ <span
1137
+ className={cn(
1138
+ 'px-1.5 py-0.5 text-[7px] font-semibold uppercase tracking-wide rounded',
1139
+ getBadgeClasses(fav.badge)
1140
+ )}
1141
+ >
1142
+ {fav.badge}
1143
+ </span>
1144
+ )}
1145
+ <div
1146
+ onClick={(e) => {
1147
+ e.stopPropagation();
1148
+ if (node) {
1149
+ toggleFavorite(fav.nodeKey, node);
1150
+ } else {
1151
+ // Node no longer exists, remove directly using nodeKey
1152
+ removeFavoriteAction(fav.nodeKey);
1153
+ }
1154
+ }}
1155
+ className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1156
+ >
1157
+ <Star className="w-3.5 h-3.5 fill-current" />
1158
+ </div>
1159
+ {node?.pages && node.pages.length > 0 && (
1160
+ <span className="flex items-center justify-center w-5 h-5 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-content-text))]">
1161
+ <ChevronRight className="w-4 h-4" />
1162
+ </span>
1163
+ )}
1164
+ </div>
1165
+ </button>
1166
+ );
1167
+ })}
1168
+ </div>
1169
+ </div>
1170
+ )}
1171
+
1172
+ {/* Empty State */}
1173
+ {currentLevel.entries.length === 0 && favorites.length === 0 && (
1174
+ <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1175
+ <div className="mb-4 p-3 rounded-full bg-[rgb(var(--ec-group-icon-bg))]">
1176
+ <FileQuestion className="w-8 h-8 text-[rgb(var(--ec-icon-color))]" />
1177
+ </div>
1178
+ <h3 className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] mb-2">Your catalog is empty</h3>
1179
+ <p className="text-[11px] text-[rgb(var(--ec-content-text-muted))] leading-relaxed max-w-[240px]">
1180
+ Navigation will appear here when you add resources to your EventCatalog.
1181
+ </p>
1182
+ </div>
1183
+ )}
1184
+
1185
+ {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1186
+ </nav>
1214
1187
 
1215
1188
  {/* Animation keyframes */}
1216
1189
  <style>{`