@eventcatalog/core 3.29.2 → 3.31.1

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 (113) 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-7IGMIOQF.js} +1 -1
  6. package/dist/{chunk-EGQGCB2B.js → chunk-HVOLSUC2.js} +1 -1
  7. package/dist/{chunk-DB4IQ3GB.js → chunk-LWVHWR77.js} +1 -1
  8. package/dist/{chunk-VEUNSJ6Z.js → chunk-QIJOBQZ7.js} +1 -1
  9. package/dist/{chunk-MEJOYC5Z.js → chunk-UY5QDWK7.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 +11 -7
  19. package/eventcatalog/public/logo.png +0 -0
  20. package/eventcatalog/src/components/CopyAsMarkdown.tsx +29 -24
  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/Design/Design.astro +1 -1
  29. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +15 -5
  30. package/eventcatalog/src/components/MDX/Tiles/Tile.astro +11 -8
  31. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +164 -53
  32. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +1 -1
  33. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +4 -4
  34. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +12 -10
  35. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +48 -77
  36. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +238 -169
  37. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +189 -230
  38. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +39 -36
  39. package/eventcatalog/src/components/Search/Search.astro +1 -1
  40. package/eventcatalog/src/components/Seo.astro +1 -1
  41. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +218 -0
  42. package/eventcatalog/src/components/Settings/BillingSettingsForm.tsx +265 -0
  43. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +371 -0
  44. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +183 -0
  45. package/eventcatalog/src/components/Settings/LogoUpload.tsx +137 -0
  46. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +91 -0
  47. package/eventcatalog/src/components/Settings/ReadOnlyBanner.tsx +18 -0
  48. package/eventcatalog/src/components/Settings/Row.tsx +59 -0
  49. package/eventcatalog/src/components/Settings/SettingsShared.tsx +176 -0
  50. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +3 -3
  51. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +233 -261
  52. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +116 -68
  53. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +2 -2
  54. package/eventcatalog/src/components/Tables/Discover/columns.tsx +130 -197
  55. package/eventcatalog/src/components/Tables/Table.tsx +21 -18
  56. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +79 -131
  57. package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +104 -175
  58. package/eventcatalog/src/content.config.ts +1 -1
  59. package/eventcatalog/src/enterprise/auth/error.astro +1 -1
  60. package/eventcatalog/src/enterprise/auth/login.astro +1 -1
  61. package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +11 -7
  62. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +97 -95
  63. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +232 -181
  64. package/eventcatalog/src/enterprise/feature.ts +2 -1
  65. package/eventcatalog/src/enterprise/fields/pages/fields.astro +10 -8
  66. package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +0 -8
  67. package/eventcatalog/src/layouts/DirectoryLayout.astro +17 -88
  68. package/eventcatalog/src/layouts/SettingsLayout.astro +116 -0
  69. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +562 -141
  70. package/eventcatalog/src/layouts/VisualiserLayout.astro +7 -2
  71. package/eventcatalog/src/pages/_index.astro +253 -256
  72. package/eventcatalog/src/pages/api/settings/ai.ts +57 -0
  73. package/eventcatalog/src/pages/api/settings/general.ts +71 -0
  74. package/eventcatalog/src/pages/api/settings/logo.ts +113 -0
  75. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -3
  76. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +223 -73
  77. package/eventcatalog/src/pages/discover/[type]/index.astro +22 -141
  78. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +130 -30
  79. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +147 -53
  80. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +6 -2
  81. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +2 -2
  82. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +22 -19
  83. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +71 -61
  84. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +5 -1
  85. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +3 -3
  86. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +6 -32
  87. package/eventcatalog/src/pages/docs/llm/llms.txt.ts +5 -1
  88. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +11 -4
  89. package/eventcatalog/src/pages/docs/users/[id]/index.astro +12 -5
  90. package/eventcatalog/src/pages/schemas/explorer/index.astro +10 -8
  91. package/eventcatalog/src/pages/settings/assistant.astro +37 -0
  92. package/eventcatalog/src/pages/settings/billing.astro +17 -0
  93. package/eventcatalog/src/pages/settings/general.astro +32 -0
  94. package/eventcatalog/src/pages/settings/index.astro +21 -0
  95. package/eventcatalog/src/pages/settings/llm-access.astro +34 -0
  96. package/eventcatalog/src/pages/settings/mcp.astro +14 -0
  97. package/eventcatalog/src/pages/studio.astro +1 -1
  98. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +2 -7
  99. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +2 -2
  100. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +2 -7
  101. package/eventcatalog/src/styles/theme.css +95 -30
  102. package/eventcatalog/src/styles/themes/forest.css +17 -9
  103. package/eventcatalog/src/styles/themes/ocean.css +10 -2
  104. package/eventcatalog/src/styles/themes/sapphire.css +10 -2
  105. package/eventcatalog/src/styles/themes/sunset.css +25 -17
  106. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +13 -0
  107. package/eventcatalog/src/utils/eventcatalog-config/config-schema.ts +49 -0
  108. package/eventcatalog/src/utils/eventcatalog-config/config-writer.ts +149 -0
  109. package/eventcatalog/src/utils/url-builder.ts +4 -2
  110. package/package.json +7 -5
  111. package/eventcatalog/public/logo.svg +0 -14
  112. package/eventcatalog/src/enterprise/plans/index.astro +0 -319
  113. package/eventcatalog/src/pages/docs/llm/llms-services.txt.ts +0 -81
@@ -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-rail-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-rail-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
  */
@@ -777,10 +749,9 @@ export default function NestedSideBar() {
777
749
  )}
778
750
  <span
779
751
  className={cn(
780
- 'tracking-tight',
781
752
  isSubtleGroup
782
- ? 'text-[12px] text-[rgb(var(--ec-content-text-muted))] font-medium'
783
- : 'text-[13px] text-[rgb(var(--ec-content-text))] font-semibold'
753
+ ? 'text-[10px] font-semibold uppercase tracking-[0.12em] text-[rgb(var(--ec-content-text-muted))]'
754
+ : 'text-[12px] font-semibold tracking-tight text-[rgb(var(--ec-content-text))]'
784
755
  )}
785
756
  >
786
757
  {group.title}
@@ -799,7 +770,7 @@ export default function NestedSideBar() {
799
770
  );
800
771
 
801
772
  return (
802
- <div key={`group-${groupKey || index}`} className={cn(isSubtleGroup ? 'mb-3 last:mb-1' : 'mb-5 last:mb-2')}>
773
+ <div key={`group-${groupKey || index}`} className={cn(isSubtleGroup ? 'mb-2 last:mb-1' : 'mb-5 last:mb-2')}>
803
774
  {canCollapse ? (
804
775
  <button
805
776
  onClick={() => toggleSectionCollapse(groupId)}
@@ -821,7 +792,7 @@ export default function NestedSideBar() {
821
792
  <div
822
793
  className={cn(
823
794
  'flex flex-col gap-0.5 border-[rgb(var(--ec-content-border))]',
824
- isSubtleGroup ? 'border-l ml-3 mt-0.5' : shouldFlattenSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
795
+ isSubtleGroup ? 'mt-0.5' : shouldFlattenSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
825
796
  )}
826
797
  >
827
798
  {visibleChildren.map((childRef, childIndex) => {
@@ -891,7 +862,7 @@ export default function NestedSideBar() {
891
862
  {item.leftIcon && <img src={resolveIconUrl(item.leftIcon)} alt="" loading="lazy" className="w-4 h-4 flex-shrink-0" />}
892
863
  <span
893
864
  className={cn(
894
- 'text-[13px] truncate',
865
+ 'text-[12px] truncate',
895
866
  isActive
896
867
  ? 'text-[rgb(var(--ec-accent-text))] font-medium'
897
868
  : 'text-[rgb(var(--ec-content-text-secondary))] group-hover:text-[rgb(var(--ec-content-text))]'
@@ -924,11 +895,9 @@ export default function NestedSideBar() {
924
895
  );
925
896
 
926
897
  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))]';
898
+ 'group flex items-center justify-between w-full px-3 py-1.5 border border-transparent cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] active:bg-[rgb(var(--ec-content-hover))]';
928
899
  const parentClasses = itemHasChildren ? 'font-medium' : '';
929
- const activeClasses = isActive
930
- ? 'bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] rounded-none!'
931
- : '';
900
+ const activeClasses = isActive ? 'bg-[rgb(var(--ec-rail-active-bg))] hover:bg-[rgb(var(--ec-rail-active-bg))]' : '';
932
901
 
933
902
  // Leaf item with href → render as link
934
903
  if (item.href && !itemHasChildren) {
@@ -966,251 +935,254 @@ export default function NestedSideBar() {
966
935
  };
967
936
 
968
937
  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
- }}
938
+ <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-rail-bg))]">
939
+ {isTopLevel && (
940
+ <div className="flex h-[60px] items-center px-6 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
941
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text)/0.5)] truncate">
942
+ All resources
943
+ </span>
944
+ </div>
945
+ )}
946
+
947
+ {!isTopLevel && (
948
+ <div
949
+ className="flex h-[60px] items-center px-4 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10"
950
+ onMouseEnter={() => !isTopLevel && setShowPathPreview(true)}
951
+ onMouseLeave={() => {
952
+ setShowPathPreview(false);
953
+ setShowFullPath(false);
954
+ }}
955
+ >
956
+ <button
957
+ onClick={navigateBack}
958
+ disabled={isTopLevel}
959
+ className={cn(
960
+ 'flex items-center gap-2 w-full px-2 py-1.5 -mx-2 rounded-md transition-colors',
961
+ !isTopLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
962
+ isTopLevel && 'cursor-default'
963
+ )}
964
+ >
965
+ <span
966
+ className={cn(
967
+ 'flex items-center justify-center w-5 h-5 text-[rgb(var(--ec-icon-color))] transition-all',
968
+ isTopLevel && 'opacity-0',
969
+ !isTopLevel && 'group-hover:-translate-x-0.5'
970
+ )}
984
971
  >
985
- <button
986
- onClick={navigateBack}
987
- disabled={isTopLevel}
972
+ <ChevronLeft className="w-4 h-4" />
973
+ </span>
974
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text))] truncate">
975
+ {currentLevel.title}
976
+ </span>
977
+ {currentLevel.badge && (
978
+ <span
988
979
  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'
980
+ 'ml-auto px-2 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
981
+ getBadgeClasses(currentLevel.badge)
992
982
  )}
993
983
  >
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
- )}
984
+ {currentLevel.badge}
985
+ </span>
986
+ )}
987
+ </button>
1116
988
 
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>
989
+ {/* Path Preview Dropdown */}
990
+ {showPathPreview && navigationStack.length > 1 && (
991
+ <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">
992
+ <div className="px-4 py-3">
993
+ <div className="text-[9px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide mb-2">
994
+ Navigation Path
1132
995
  </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">
996
+ <div className="flex flex-col gap-0.5">
997
+ {(() => {
998
+ const SHOW_FIRST = 2; // Show first N items
999
+ const SHOW_LAST = 2; // Show last N items (including current)
1000
+ const totalItems = navigationStack.length;
1001
+ const hiddenCount = totalItems - SHOW_FIRST - SHOW_LAST;
1002
+ const shouldTruncate = hiddenCount > 0 && !showFullPath;
1003
+
1004
+ const renderPathItem = (level: NavigationLevel, index: number, displayIndex: number) => {
1005
+ const isCurrentLevel = index === navigationStack.length - 1;
1006
+ return (
1007
+ <button
1008
+ key={`path-${index}`}
1009
+ onClick={() => navigateToLevel(index)}
1010
+ disabled={isCurrentLevel}
1011
+ className={cn(
1012
+ 'flex items-center gap-2 px-2 py-1.5 rounded text-left transition-colors',
1013
+ !isCurrentLevel && 'hover:bg-[rgb(var(--ec-content-hover))] cursor-pointer',
1014
+ isCurrentLevel && 'bg-[rgb(var(--ec-content-hover))] cursor-default'
1015
+ )}
1016
+ style={{ paddingLeft: `${displayIndex * 12 + 8}px` }}
1017
+ >
1018
+ {index === 0 ? (
1019
+ <Home className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] flex-shrink-0" />
1020
+ ) : (
1021
+ <ChevronRight className="w-3.5 h-3.5 text-[rgb(var(--ec-content-text-muted))] flex-shrink-0" />
1022
+ )}
1148
1023
  <span
1149
1024
  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))]'
1025
+ 'text-[12px] truncate',
1026
+ isCurrentLevel
1027
+ ? 'font-medium text-[rgb(var(--ec-content-text))]'
1028
+ : 'text-[rgb(var(--ec-content-text-secondary))]'
1154
1029
  )}
1155
1030
  >
1156
- {fav.title}
1031
+ {level.title}
1157
1032
  </span>
1158
- </div>
1159
- <div className="flex items-center gap-1 flex-shrink-0">
1160
- {fav.badge && (
1033
+ {level.badge && (
1161
1034
  <span
1162
1035
  className={cn(
1163
- 'px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
1164
- getBadgeClasses(fav.badge)
1036
+ 'ml-auto px-1.5 py-0.5 text-[7px] font-semibold uppercase tracking-wide rounded flex-shrink-0',
1037
+ getBadgeClasses(level.badge)
1165
1038
  )}
1166
1039
  >
1167
- {fav.badge}
1040
+ {level.badge}
1168
1041
  </span>
1169
1042
  )}
1170
- <div
1043
+ </button>
1044
+ );
1045
+ };
1046
+
1047
+ if (shouldTruncate) {
1048
+ return (
1049
+ <>
1050
+ {/* First N items */}
1051
+ {navigationStack.slice(0, SHOW_FIRST).map((level, index) => renderPathItem(level, index, index))}
1052
+
1053
+ {/* Collapsed middle section */}
1054
+ <button
1171
1055
  onClick={(e) => {
1172
1056
  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
- }
1057
+ setShowFullPath(true);
1179
1058
  }}
1180
- className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1059
+ 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"
1060
+ style={{ paddingLeft: `${SHOW_FIRST * 12 + 8}px` }}
1181
1061
  >
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" />
1062
+ <span className="flex items-center justify-center w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))]">
1063
+ <span className="text-xs">•••</span>
1187
1064
  </span>
1188
- )}
1189
- </div>
1190
- </button>
1191
- );
1192
- })}
1065
+ <span className="text-[12px] text-[rgb(var(--ec-content-text-muted))]">
1066
+ {hiddenCount} more level{hiddenCount > 1 ? 's' : ''}
1067
+ </span>
1068
+ <ChevronDown className="w-3.5 h-3.5 text-[rgb(var(--ec-icon-color))] ml-auto" />
1069
+ </button>
1070
+
1071
+ {/* Last N items */}
1072
+ {navigationStack.slice(-SHOW_LAST).map((level, sliceIndex) => {
1073
+ const actualIndex = totalItems - SHOW_LAST + sliceIndex;
1074
+ return renderPathItem(level, actualIndex, SHOW_FIRST + 1 + sliceIndex);
1075
+ })}
1076
+ </>
1077
+ );
1078
+ }
1079
+
1080
+ // Show full path
1081
+ return navigationStack.map((level, index) => renderPathItem(level, index, index));
1082
+ })()}
1193
1083
  </div>
1194
1084
  </div>
1195
- )}
1085
+ </div>
1086
+ )}
1087
+ </div>
1088
+ )}
1196
1089
 
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
- )}
1090
+ {/* Navigation Content */}
1091
+ <nav
1092
+ key={animationKey}
1093
+ className={cn('flex-1 overflow-y-auto overflow-x-hidden p-4 px-2', getAnimationClass())}
1094
+ style={{
1095
+ scrollbarWidth: 'thin',
1096
+ scrollbarColor: 'rgb(var(--ec-content-border)) transparent',
1097
+ }}
1098
+ >
1099
+ {/* Favorites Section */}
1100
+ {favorites.length > 0 && isTopLevel && (
1101
+ <div className="mb-6">
1102
+ <div className="flex items-center px-2 py-1.5">
1103
+ <Star className="w-3.5 h-3.5 mr-2 text-amber-400 fill-current" />
1104
+ <span className="text-[12px] text-[rgb(var(--ec-content-text))] font-semibold">Favorites</span>
1105
+ </div>
1106
+ <div className="flex flex-col gap-0.5 border-l ml-3.5 border-amber-200">
1107
+ {favorites.map((fav, index) => {
1108
+ const node = resolveRef(fav.nodeKey);
1109
+ const isActive = fav.href && currentPath === fav.href;
1209
1110
 
1210
- {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1211
- </nav>
1212
- </>
1213
- )}
1111
+ return (
1112
+ <button
1113
+ key={`fav-${index}`}
1114
+ onClick={() => navigateToFavorite(fav)}
1115
+ className={cn(
1116
+ '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',
1117
+ isActive &&
1118
+ 'border-[rgb(var(--ec-accent)/0.16)] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] shadow-sm'
1119
+ )}
1120
+ >
1121
+ <div className="flex items-center gap-2.5 min-w-0 flex-1">
1122
+ <span
1123
+ className={cn(
1124
+ 'text-[12px] truncate',
1125
+ isActive
1126
+ ? 'text-[rgb(var(--ec-accent-text))] font-medium'
1127
+ : 'text-[rgb(var(--ec-content-text-secondary))] group-hover:text-[rgb(var(--ec-content-text))]'
1128
+ )}
1129
+ >
1130
+ {fav.title}
1131
+ </span>
1132
+ </div>
1133
+ <div className="flex items-center gap-1 flex-shrink-0">
1134
+ {fav.badge && (
1135
+ <span
1136
+ className={cn(
1137
+ 'px-1.5 py-0.5 text-[7px] font-semibold uppercase tracking-wide rounded',
1138
+ getBadgeClasses(fav.badge)
1139
+ )}
1140
+ >
1141
+ {fav.badge}
1142
+ </span>
1143
+ )}
1144
+ <div
1145
+ onClick={(e) => {
1146
+ e.stopPropagation();
1147
+ if (node) {
1148
+ toggleFavorite(fav.nodeKey, node);
1149
+ } else {
1150
+ // Node no longer exists, remove directly using nodeKey
1151
+ removeFavoriteAction(fav.nodeKey);
1152
+ }
1153
+ }}
1154
+ className="flex items-center justify-center w-5 h-5 text-amber-400 hover:text-amber-500 rounded transition-colors cursor-pointer"
1155
+ >
1156
+ <Star className="w-3.5 h-3.5 fill-current" />
1157
+ </div>
1158
+ {node?.pages && node.pages.length > 0 && (
1159
+ <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))]">
1160
+ <ChevronRight className="w-4 h-4" />
1161
+ </span>
1162
+ )}
1163
+ </div>
1164
+ </button>
1165
+ );
1166
+ })}
1167
+ </div>
1168
+ </div>
1169
+ )}
1170
+
1171
+ {/* Empty State */}
1172
+ {currentLevel.entries.length === 0 && favorites.length === 0 && (
1173
+ <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1174
+ <div className="mb-4 p-3 rounded-full bg-[rgb(var(--ec-group-icon-bg))]">
1175
+ <FileQuestion className="w-8 h-8 text-[rgb(var(--ec-icon-color))]" />
1176
+ </div>
1177
+ <h3 className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] mb-2">Your catalog is empty</h3>
1178
+ <p className="text-[11px] text-[rgb(var(--ec-content-text-muted))] leading-relaxed max-w-[240px]">
1179
+ Navigation will appear here when you add resources to your EventCatalog.
1180
+ </p>
1181
+ </div>
1182
+ )}
1183
+
1184
+ {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1185
+ </nav>
1214
1186
 
1215
1187
  {/* Animation keyframes */}
1216
1188
  <style>{`