@eventcatalog/core 5.0.0-beta.2 → 5.0.0-beta.20

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 (204) hide show
  1. package/LICENSE +234 -134
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +71 -35
  8. package/dist/analytics/log-build.js +5 -4
  9. package/dist/analytics/telemetry.cjs +42 -0
  10. package/dist/analytics/telemetry.d.cts +20 -0
  11. package/dist/analytics/telemetry.d.ts +20 -0
  12. package/dist/analytics/telemetry.js +8 -0
  13. package/dist/catalog-runtime.cjs +1 -1
  14. package/dist/{chunk-HGRFXTFC.js → chunk-3VQSVQTU.js} +1 -1
  15. package/dist/{chunk-KIDISU6X.js → chunk-5ZUN2TXB.js} +56 -33
  16. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  17. package/dist/chunk-CPW6G62V.js +17 -0
  18. package/dist/{chunk-4J2ZM2HX.js → chunk-F236OJZD.js} +1 -1
  19. package/dist/{chunk-XARPIXWF.js → chunk-FWH5T6U4.js} +9 -3
  20. package/dist/{chunk-62EV4IJA.js → chunk-R2X4ZEF6.js} +1 -1
  21. package/dist/constants.cjs +1 -1
  22. package/dist/constants.js +1 -1
  23. package/dist/eventcatalog.cjs +214 -129
  24. package/dist/eventcatalog.config.d.cts +59 -1
  25. package/dist/eventcatalog.config.d.ts +59 -1
  26. package/dist/eventcatalog.js +39 -15
  27. package/dist/federation/federate.js +2 -2
  28. package/dist/generate.cjs +1 -1
  29. package/dist/generate.js +3 -3
  30. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  31. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  32. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  33. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  34. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  35. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  36. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  37. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  38. package/dist/runtime-dependencies/manifest.json +13 -1
  39. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  40. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  41. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  42. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  43. package/dist/runtime-dependencies/yjs.mjs +3 -0
  44. package/dist/utils/cli-logger.cjs +1 -1
  45. package/dist/utils/cli-logger.js +2 -2
  46. package/dist/utils/license-status.cjs +71 -45
  47. package/dist/utils/license-status.d.cts +14 -5
  48. package/dist/utils/license-status.d.ts +14 -5
  49. package/dist/utils/license-status.js +1 -1
  50. package/eventcatalog/astro.config.mjs +23 -1
  51. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  52. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  53. package/eventcatalog/integrations/storage.mjs +20 -0
  54. package/eventcatalog/integrations/studio-server.mjs +32 -0
  55. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  56. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  57. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  58. package/eventcatalog/src/components/Header.astro +3 -3
  59. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  60. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
  61. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  62. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  63. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  64. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  65. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  66. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  67. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  68. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  69. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  70. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  71. package/eventcatalog/src/components/Seo.astro +5 -1
  72. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  73. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  74. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  75. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  76. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  77. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  78. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  79. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  80. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  81. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  82. package/eventcatalog/src/features/auth/error.astro +4 -2
  83. package/eventcatalog/src/features/auth/login.astro +2 -1
  84. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  85. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  86. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  87. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  88. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  89. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  90. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  91. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  92. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  93. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +77 -76
  94. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  95. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  96. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  97. package/eventcatalog/src/features/server/start.ts +25 -0
  98. package/eventcatalog/src/features/storage/database.ts +88 -0
  99. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  100. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  101. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  102. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  103. package/eventcatalog/src/features/storage/sql.ts +28 -0
  104. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  105. package/eventcatalog/src/features/studio/agents.ts +28 -0
  106. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  107. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  108. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  109. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  110. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  111. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  112. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  113. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  114. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  115. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  116. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  117. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  118. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  119. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  120. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +203 -0
  121. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  122. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  123. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  124. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  125. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  126. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  127. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  128. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  129. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  130. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  131. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  132. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  133. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  134. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  135. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  136. package/eventcatalog/src/features/studio/identity.ts +59 -0
  137. package/eventcatalog/src/features/studio/limits.ts +2 -0
  138. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  139. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  140. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  141. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  142. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  143. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  144. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  145. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  146. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  147. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  148. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  149. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -7
  150. package/eventcatalog/src/pages/_index.astro +1 -1
  151. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  152. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  153. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  154. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  155. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  156. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  157. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  158. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +1 -1
  159. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  160. package/eventcatalog/src/pages/index.astro +1 -1
  161. package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
  162. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +1 -1
  163. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  164. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  165. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  166. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  167. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  168. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  169. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  170. package/eventcatalog/src/styles/tailwind.css +2 -2
  171. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  172. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  173. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  174. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  175. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  176. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  177. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  178. package/eventcatalog/src/utils/collections/util.ts +31 -0
  179. package/eventcatalog/src/utils/feature.ts +8 -0
  180. package/eventcatalog/src/utils/git.ts +11 -5
  181. package/eventcatalog/src/utils/link-validation.ts +25 -19
  182. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  183. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  184. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  185. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  186. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  187. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  188. package/eventcatalog/src/utils/page-title.ts +15 -0
  189. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  190. package/eventcatalog/src/utils/trial.ts +1 -1
  191. package/package.json +22 -16
  192. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  193. package/eventcatalog/public/studio-bg.png +0 -0
  194. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  195. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  196. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  197. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  198. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  199. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  200. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  203. package/eventcatalog/src/pages/studio.astro +0 -316
  204. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -31,11 +31,14 @@ import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
31
31
  import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
32
32
  import { getNodesAndEdges as getNodesAndEdgesForChannel } from '@utils/node-graphs/channel-node-graph';
33
33
  import config from '@config';
34
- import { isEventCatalogChatVisible, isDevMode } from '@utils/feature';
34
+ import { isEventCatalogChatVisible, isDevMode, isCanvasEnabled } from '@utils/feature';
35
+ import { CANVASES_API_PATH } from '@features/studio/api/canvases-api';
35
36
  import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
36
37
  import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
37
38
 
38
39
  const isChatEnabled = isEventCatalogChatVisible();
40
+ // With Studio on, the menu can start a canvas from the diagram
41
+ const studioApiUrl = isCanvasEnabled() ? buildUrl(CANVASES_API_PATH, true) : undefined;
39
42
 
40
43
  interface Props {
41
44
  id: string;
@@ -257,6 +260,7 @@ if (overview && levelsOverview) {
257
260
  hiddenMessagesGraph={hiddenMessagesGraph}
258
261
  preferenceScope={collection}
259
262
  swimlanes={lanes}
263
+ studioApiUrl={studioApiUrl}
260
264
  />
261
265
  </div>
262
266
 
@@ -22,12 +22,19 @@
22
22
  opacity: 1;
23
23
  }
24
24
 
25
+ /* On every page, hidden until a navigation takes a while: paused until then (a running animation costs a style
26
+ recalc and paint on every frame, even when it can't be seen) */
25
27
  .navigation-progress__bar {
26
28
  width: 35%;
27
29
  height: 100%;
28
30
  background: rgb(var(--ec-accent));
29
31
  box-shadow: 0 0 10px rgb(var(--ec-accent) / 0.55);
30
32
  animation: navigation-progress 1.1s ease-in-out infinite;
33
+ animation-play-state: paused;
34
+ }
35
+
36
+ #navigation-progress[data-visible='true'] .navigation-progress__bar {
37
+ animation-play-state: running;
31
38
  }
32
39
 
33
40
  :global([data-navigation-pending='true']) {
@@ -4,6 +4,7 @@ import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
4
4
  import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
5
5
  import { getLanguageForHighlight, getSchemaTypeLabel } from './utils';
6
6
  import { useDarkMode } from './useDarkMode';
7
+ import { renderHighlightedRows } from '@utils/syntax-highlighter-renderer';
7
8
  import type { SchemaItem } from './types';
8
9
 
9
10
  interface ApiContentViewerProps {
@@ -135,6 +136,7 @@ export default function ApiContentViewer({ message, onCopy, copiedId }: ApiConte
135
136
 
136
137
  <div className="border-t" style={{ borderColor: dividerColor }}>
137
138
  <SyntaxHighlighter
139
+ renderer={renderHighlightedRows}
138
140
  language={getLanguageForHighlight(message.schemaExtension)}
139
141
  style={isDarkMode ? oneDark : oneLight}
140
142
  customStyle={{
@@ -13,6 +13,7 @@ import { MarkdownCodeGroup } from '../MDX/CodeGroup/MarkdownCodeGroup';
13
13
  import { MarkdownColumns, MarkdownColumn } from '../MDX/Columns/MarkdownColumns';
14
14
  import { remarkMarkdownColumns } from '../../remark-plugins/markdown-columns';
15
15
  import { getLanguageForHighlight } from './utils';
16
+ import { renderHighlightedRows } from '@utils/syntax-highlighter-renderer';
16
17
 
17
18
  interface ExamplesViewerProps {
18
19
  examples: MessageExample[];
@@ -40,6 +41,7 @@ function CodeBlock({ language, code }: { language?: string; code: string }) {
40
41
  const content = code.replace(/\s+$/, '');
41
42
  const highlighter = (
42
43
  <SyntaxHighlighter
44
+ renderer={renderHighlightedRows}
43
45
  language={language || 'text'}
44
46
  style={isDarkMode ? oneDark : oneLight}
45
47
  customStyle={{
@@ -6,6 +6,7 @@ import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
6
6
  import { getLanguageForHighlight } from './utils';
7
7
  import type { SchemaItem } from './types';
8
8
  import { useDarkMode } from './useDarkMode';
9
+ import { renderHighlightedRows } from '@utils/syntax-highlighter-renderer';
9
10
 
10
11
  interface SchemaCodeModalProps {
11
12
  isOpen: boolean;
@@ -60,6 +61,7 @@ export default function SchemaCodeModal({ isOpen, onOpenChange, message, onCopy,
60
61
  {/* Content */}
61
62
  <div className="flex-1 overflow-auto p-6">
62
63
  <SyntaxHighlighter
64
+ renderer={renderHighlightedRows}
63
65
  language={getLanguageForHighlight(message.schemaExtension)}
64
66
  style={isDarkMode ? oneDark : oneLight}
65
67
  customStyle={{
@@ -9,6 +9,7 @@ import ProtobufSchemaViewer from './ProtobufSchemaViewer';
9
9
  import { getLanguageForHighlight, SCHEMA_CODE_STYLE } from './utils';
10
10
  import type { SchemaItem } from './types';
11
11
  import { useDarkMode } from './useDarkMode';
12
+ import { renderHighlightedRows } from '@utils/syntax-highlighter-renderer';
12
13
 
13
14
  interface SchemaContentViewerProps {
14
15
  message: SchemaItem;
@@ -112,6 +113,7 @@ export default function SchemaContentViewer({
112
113
 
113
114
  <div className="min-h-0 flex-1 overflow-auto">
114
115
  <SyntaxHighlighter
116
+ renderer={renderHighlightedRows}
115
117
  language={getLanguageForHighlight(message.schemaExtension)}
116
118
  style={isDarkMode ? oneDark : oneLight}
117
119
  customStyle={{ ...SCHEMA_CODE_STYLE, height: '100%' }}
@@ -384,6 +384,9 @@ export default function SchemaDetailsPanel({
384
384
  [diffToVersion, uniqueAvailableVersions]
385
385
  );
386
386
  const comparing = (activeTab === 'versions' && hasMultipleVersions) || isDiffModalOpen;
387
+ const contentReady = !content.loading && !content.error;
388
+ // Server-rendered examples stay mounted and are only shown on the Examples tab (see the tab content below).
389
+ const showRenderedExamples = !!renderedExamples && activeTab === 'examples' && contentReady;
387
390
 
388
391
  const compareWithCurrent = (version: string) => {
389
392
  setDiffFromVersion(version);
@@ -705,11 +708,16 @@ export default function SchemaDetailsPanel({
705
708
 
706
709
  {/* Tab content */}
707
710
  <div className="flex-1 min-h-0 overflow-hidden p-6">
708
- {content.loading || content.error ? (
711
+ {/* Always render the examples slot and hide it on other tabs. Astro moves a slot missing from the
712
+ server HTML into a <template>, where its island scripts never run and no island on the page hydrates. */}
713
+ {renderedExamples && (
714
+ <div className="h-full overflow-auto pr-1" hidden={!showRenderedExamples}>
715
+ {renderedExamples}
716
+ </div>
717
+ )}
718
+ {!contentReady ? (
709
719
  <SchemaLoadingState loading={content.loading} error={content.error} retry={content.retry} />
710
- ) : activeTab === 'examples' && renderedExamples ? (
711
- <div className="h-full overflow-auto pr-1">{renderedExamples}</div>
712
- ) : activeTab === 'examples' && examples.length > 0 ? (
720
+ ) : showRenderedExamples ? null : activeTab === 'examples' && examples.length > 0 ? (
713
721
  <ExamplesViewer examples={examples} />
714
722
  ) : activeTab === 'metadata' ? (
715
723
  <SchemaMetadata message={message} owners={showOwners ? owners : []} />
@@ -16,34 +16,22 @@
16
16
  import { setSidebarData, sidebarStore } from '@stores/sidebar-store';
17
17
  import { buildUrl } from '@utils/url-builder';
18
18
 
19
- // Only fetch if we haven't already loaded the data
19
+ // Only fetch if we haven't already loaded the data. Fetched straight away rather than when
20
+ // the browser is idle: the sidebar's first paint comes from the server (see SideNav.astro),
21
+ // but drilling down and search wait on this data
20
22
  if (!sidebarStore.get()) {
21
- const apiUrl = buildUrl('/api/sidebar-data.json', true);
22
-
23
- const loadSidebarData = () => {
24
- // Double-check cache in case another component loaded data while waiting for idle
25
- if (sidebarStore.get()) return;
26
-
27
- fetch(apiUrl)
28
- .then((response) => {
29
- if (!response.ok) {
30
- throw new Error(`Failed to fetch sidebar data: ${response.status}`);
31
- }
32
- return response.json();
33
- })
34
- .then((data) => {
35
- setSidebarData(data);
36
- })
37
- .catch((error) => {
38
- console.error('Error loading sidebar data:', error);
39
- });
40
- };
41
-
42
- // Defer non-critical sidebar payload fetch until browser is idle
43
- if (typeof window.requestIdleCallback === 'function') {
44
- window.requestIdleCallback(loadSidebarData, { timeout: 1200 });
45
- } else {
46
- setTimeout(loadSidebarData, 300);
47
- }
23
+ fetch(buildUrl('/api/sidebar-data.json', true))
24
+ .then((response) => {
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to fetch sidebar data: ${response.status}`);
27
+ }
28
+ return response.json();
29
+ })
30
+ .then((data) => {
31
+ setSidebarData(data);
32
+ })
33
+ .catch((error) => {
34
+ console.error('Error loading sidebar data:', error);
35
+ });
48
36
  }
49
37
  </script>
@@ -34,6 +34,7 @@ import {
34
34
  getUrlForSearchItem,
35
35
  highlightQuery,
36
36
  mapPagefindResultsToSearchItems,
37
+ rankResourceSearchItems,
37
38
  type SearchItem,
38
39
  type SearchNode,
39
40
  } from './search-utils';
@@ -316,16 +317,7 @@ export default function SearchModal() {
316
317
  return items;
317
318
  }
318
319
 
319
- const lowerQuery = query.toLowerCase();
320
- return items.filter((item) => {
321
- if (item.name.toLowerCase().includes(lowerQuery)) return true;
322
- // Key format is "type:id:version" (e.g. "service:OrdersService:0.0.3").
323
- // Match against the id so users can find resources by their raw id too.
324
- const keyParts = item.key?.split(':') ?? [];
325
- const id = keyParts[1];
326
- if (id?.toLowerCase().includes(lowerQuery)) return true;
327
- return !!item.rawNode.summary && item.rawNode.summary.toLowerCase().includes(lowerQuery);
328
- });
320
+ return rankResourceSearchItems(items, query);
329
321
  }, [indexedItems, isIndexedSearch, items, query]);
330
322
 
331
323
  const filters = useMemo(() => {
@@ -168,6 +168,32 @@ export const getIndexedResultRank = ({
168
168
  return 0;
169
169
  };
170
170
 
171
+ /**
172
+ * How well a resource matches a resource search query: an exact name (5), a name starting with
173
+ * the query (4) or containing it (3), then the id (2), then the summary (1). 0 is no match.
174
+ */
175
+ export const getResourceResultRank = (item: Pick<SearchItem, 'name' | 'key' | 'rawNode'>, query: string) => {
176
+ const lowerQuery = query.toLowerCase();
177
+ const name = item.name.toLowerCase();
178
+ if (name === lowerQuery) return 5;
179
+ if (name.startsWith(lowerQuery)) return 4;
180
+ if (name.includes(lowerQuery)) return 3;
181
+ // Key format is "type:id:version" (e.g. "service:OrdersService:0.0.3").
182
+ // Match against the id so users can find resources by their raw id too.
183
+ const id = item.key?.split(':')[1];
184
+ if (id?.toLowerCase().includes(lowerQuery)) return 2;
185
+ if (item.rawNode.summary?.toLowerCase().includes(lowerQuery)) return 1;
186
+ return 0;
187
+ };
188
+
189
+ /** Resources that match a resource search query, best matches first (index order within a rank) */
190
+ export const rankResourceSearchItems = <T extends Pick<SearchItem, 'name' | 'key' | 'rawNode'>>(items: T[], query: string) =>
191
+ items
192
+ .map((item, index) => ({ item, index, rank: getResourceResultRank(item, query) }))
193
+ .filter(({ rank }) => rank > 0)
194
+ .sort((a, b) => b.rank - a.rank || a.index - b.index)
195
+ .map(({ item }) => item);
196
+
171
197
  export const applyActiveFilter = <T extends { type: string }>(items: T[], activeFilter: string) => {
172
198
  if (activeFilter === 'all') {
173
199
  return items;
@@ -3,8 +3,12 @@ import { SEO } from 'astro-seo';
3
3
  import config from '@config';
4
4
  import defaultImageFile from '../../public/opengraph.png';
5
5
  import { buildUrl } from '@utils/url-builder';
6
+ import { formatPageTitle } from '@utils/page-title';
6
7
 
7
- const { title = config.title, description = config.tagline, ogTitle = title, ogType = 'website' } = Astro.props;
8
+ const { description = config.tagline, ogType = 'website' } = Astro.props;
9
+ // The page's own title (none for the homepage), followed by the catalog's
10
+ const title = formatPageTitle(Astro.props.title);
11
+ const ogTitle = title;
8
12
 
9
13
  const siteBaseURL = new URL(Astro.url);
10
14
  const defaultImage = new URL(defaultImageFile.src, siteBaseURL);
@@ -21,56 +21,56 @@ import {
21
21
  type FavoriteItem,
22
22
  } from '@stores/favorites-store';
23
23
  import {
24
+ buildNodeLookup,
24
25
  canCollapseGroup,
25
26
  findNodeKeyByUrl,
26
27
  getBadgeClasses,
27
28
  getDefaultCollapsedState,
29
+ hidesDefaultItemIcon,
28
30
  isGroupCollapsed,
31
+ resolveNavRef,
32
+ rootLevel,
33
+ stripBasePath,
29
34
  toggleGroupCollapsed,
35
+ type NavigationLevel,
36
+ type SidebarSnapshot,
30
37
  } from './utils';
31
38
  import { resolveIconUrl } from '@utils/icon';
32
39
 
33
40
  const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
34
41
 
35
- /**
36
- * Strip the configured base path from a pathname so URL pattern matching works
37
- * regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
38
- */
39
- const stripBasePath = (pathname: string): string => {
40
- const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
41
- if (base && pathname.startsWith(base)) {
42
- return pathname.slice(base.length) || '/';
43
- }
44
- return pathname;
45
- };
46
-
47
42
  // ============================================
48
43
  // Component
49
44
  // ============================================
50
45
 
51
- type NavigationLevel = {
52
- key: string | null; // The key of the node that was drilled into (null for root)
53
- entries: ChildRef[];
54
- title: string;
55
- badge?: string; // Category badge (e.g., "Domain", "Service")
46
+ type NestedSideBarProps = {
47
+ /**
48
+ * This page's level, built on the server. It renders the sidebar into the HTML and keeps
49
+ * the island showing it until the full navigation data (sidebar-data.json) is loaded.
50
+ */
51
+ snapshot?: SidebarSnapshot;
56
52
  };
57
53
 
58
- export default function NestedSideBar() {
59
- const data = useStore(sidebarStore);
54
+ export default function NestedSideBar({ snapshot }: NestedSideBarProps) {
55
+ const storeData = useStore(sidebarStore);
60
56
  const favorites = useStore(favoritesStore);
57
+ // Drill-down, restoring state and favorites need the whole catalog; until it loads the
58
+ // snapshot only shows this page's level
59
+ const hasFullData = storeData !== null;
60
+ const data = storeData ?? snapshot?.data ?? null;
61
61
 
62
62
  // Guard against undefined data (e.g., during hydration)
63
63
  // Use useMemo to ensure stable references for roots and nodes
64
64
  const roots = useMemo(() => data?.roots ?? [], [data?.roots]);
65
65
  const nodes = useMemo(() => data?.nodes ?? {}, [data?.nodes]);
66
66
 
67
- const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(() => [
68
- { key: null, entries: [], title: 'Documentation' },
69
- ]);
67
+ const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(
68
+ () => snapshot?.stack ?? [{ key: null, entries: [], title: 'Documentation' }]
69
+ );
70
70
  const [animationKey, setAnimationKey] = useState(0);
71
71
  const [slideDirection, setSlideDirection] = useState<'forward' | 'backward' | null>(null);
72
72
  const [isInitialized, setIsInitialized] = useState(false);
73
- const [currentPath, setCurrentPath] = useState<string>('');
73
+ const [currentPath, setCurrentPath] = useState<string>(snapshot?.url ?? '');
74
74
  const [sectionCollapsePreferences, setSectionCollapsePreferences] = useState(loadCollapsedSections);
75
75
  const [showPathPreview, setShowPathPreview] = useState(false);
76
76
  const [showFullPath, setShowFullPath] = useState(false);
@@ -78,33 +78,7 @@ export default function NestedSideBar() {
78
78
 
79
79
  // Build a lookup map for faster URL navigation
80
80
  // Map format: "type:id" -> "nodeKey"
81
- const nodeLookup = useMemo(() => {
82
- const lookup = new Map<string, string>();
83
-
84
- Object.entries(nodes).forEach(([key, value]) => {
85
- // Skip keys that are references (string values pointing to other keys)
86
- // These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
87
- if (typeof value === 'string') return;
88
-
89
- // Key formats:
90
- // - "type:id:version" (e.g., "service:OrdersService:0.0.3")
91
- // - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
92
- // - "list:name" (e.g., "list:domains") - skip these
93
- const parts = key.split(':');
94
-
95
- // Skip list items
96
- if (parts[0] === 'list') return;
97
-
98
- if (parts.length >= 2) {
99
- // Store as "type:id"
100
- const type = parts[0];
101
- const id = parts[1];
102
- lookup.set(`${type}:${id}`, key);
103
- }
104
- });
105
-
106
- return lookup;
107
- }, [nodes]);
81
+ const nodeLookup = useMemo(() => buildNodeLookup(nodes), [nodes]);
108
82
 
109
83
  /**
110
84
  * Toggle section collapse state
@@ -125,7 +99,7 @@ export default function NestedSideBar() {
125
99
  setNavigationStack((prevStack) => {
126
100
  // Only update if the current stack has no entries (initial state)
127
101
  if (prevStack.length === 1 && prevStack[0].entries.length === 0) {
128
- return [{ key: null, entries: roots, title: 'Documentation' }];
102
+ return [rootLevel(roots)];
129
103
  }
130
104
  return prevStack;
131
105
  });
@@ -145,21 +119,7 @@ export default function NestedSideBar() {
145
119
  * Resolve a child reference to a NavNode
146
120
  * Handles both direct keys and string references (unversioned aliases pointing to versioned keys)
147
121
  */
148
- const resolveRef = useCallback(
149
- (ref: ChildRef): NavNode | null => {
150
- if (typeof ref === 'string') {
151
- const node = nodes[ref];
152
- if (!node) return null;
153
- // If node is a string, it's a reference to another key (e.g., unversioned alias)
154
- if (typeof node === 'string') {
155
- return (nodes[node] as NavNode) ?? null;
156
- }
157
- return node;
158
- }
159
- return ref;
160
- },
161
- [nodes]
162
- );
122
+ const resolveRef = useCallback((ref: ChildRef): NavNode | null => resolveNavRef(nodes, ref), [nodes]);
163
123
 
164
124
  /**
165
125
  * Check if a node is visible (default: true)
@@ -174,7 +134,7 @@ export default function NestedSideBar() {
174
134
  */
175
135
  const buildStackFromPath = useCallback(
176
136
  (path: string[]): NavigationLevel[] => {
177
- const stack: NavigationLevel[] = [{ key: null, entries: roots, title: 'Documentation' }];
137
+ const stack: NavigationLevel[] = [rootLevel(roots)];
178
138
 
179
139
  for (const key of path) {
180
140
  const node = resolveRef(key);
@@ -280,7 +240,7 @@ export default function NestedSideBar() {
280
240
  if (foundNode && foundNode.pages && foundNode.pages.length > 0) {
281
241
  // Fallback: Flattened navigation
282
242
  return [
283
- { key: null, entries: roots, title: 'Documentation' },
243
+ rootLevel(roots),
284
244
  { key: foundNodeKey, entries: foundNode.pages, title: foundNode.title, badge: foundNode.badge },
285
245
  ];
286
246
  }
@@ -295,7 +255,7 @@ export default function NestedSideBar() {
295
255
  setSlideDirection('backward');
296
256
  setAnimationKey((prev) => prev + 1);
297
257
  }
298
- return [{ key: null, entries: roots, title: 'Documentation' }];
258
+ return [rootLevel(roots)];
299
259
  });
300
260
  return true;
301
261
  }
@@ -308,14 +268,14 @@ export default function NestedSideBar() {
308
268
  * Restore state from localStorage on mount, or navigate to URL
309
269
  */
310
270
  useEffect(() => {
311
- if (!data || roots.length === 0 || isInitialized) return;
271
+ if (!hasFullData || roots.length === 0 || isInitialized) return;
312
272
 
313
273
  const currentUrl = window.location.pathname;
314
274
  const normalizedCurrentUrl = stripBasePath(currentUrl);
315
275
 
316
276
  // Force root navigation on homepage
317
277
  if (normalizedCurrentUrl === '/' || normalizedCurrentUrl === '') {
318
- setNavigationStack([{ key: null, entries: roots, title: 'Documentation' }]);
278
+ setNavigationStack([rootLevel(roots)]);
319
279
  setIsInitialized(true);
320
280
  return;
321
281
  }
@@ -346,7 +306,7 @@ export default function NestedSideBar() {
346
306
  const targetNode = resolveRef(targetKey);
347
307
  if (targetNode && targetNode.pages && targetNode.pages.length > 0) {
348
308
  finalStack = [
349
- { key: null, entries: roots, title: 'Documentation' },
309
+ rootLevel(roots),
350
310
  { key: targetKey, entries: targetNode.pages, title: targetNode.title, badge: targetNode.badge },
351
311
  ];
352
312
  }
@@ -354,13 +314,13 @@ export default function NestedSideBar() {
354
314
 
355
315
  // 3. Fallback to root
356
316
  if (!finalStack) {
357
- setNavigationStack([{ key: null, entries: roots, title: 'Documentation' }]);
317
+ setNavigationStack([rootLevel(roots)]);
358
318
  } else {
359
319
  setNavigationStack(finalStack);
360
320
  }
361
321
 
362
322
  setIsInitialized(true);
363
- }, [data, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
323
+ }, [hasFullData, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
364
324
 
365
325
  /**
366
326
  * Save state whenever navigation changes
@@ -574,6 +534,7 @@ export default function NestedSideBar() {
574
534
  * Handle drilling down into an item with children
575
535
  */
576
536
  const handleDrillDown = (node: NavNode, nodeKey: string | null) => {
537
+ if (!hasFullData) return;
577
538
  if (node.pages && node.pages.length > 0) {
578
539
  setSlideDirection('forward');
579
540
  setAnimationKey((prev) => prev + 1);
@@ -589,7 +550,7 @@ export default function NestedSideBar() {
589
550
  * Navigate back one level
590
551
  */
591
552
  const navigateBack = () => {
592
- if (navigationStack.length > 1) {
553
+ if (hasFullData && navigationStack.length > 1) {
593
554
  setSlideDirection('backward');
594
555
  setAnimationKey((prev) => prev + 1);
595
556
  setNavigationStack(navigationStack.slice(0, -1));
@@ -603,7 +564,7 @@ export default function NestedSideBar() {
603
564
  * Navigate to a specific level in the stack
604
565
  */
605
566
  const navigateToLevel = (levelIndex: number) => {
606
- if (levelIndex < navigationStack.length - 1) {
567
+ if (hasFullData && levelIndex < navigationStack.length - 1) {
607
568
  setSlideDirection('backward');
608
569
  setAnimationKey((prev) => prev + 1);
609
570
  setNavigationStack(navigationStack.slice(0, levelIndex + 1));
@@ -633,6 +594,7 @@ export default function NestedSideBar() {
633
594
  * Navigate to a favorited item
634
595
  */
635
596
  const navigateToFavorite = (favorite: FavoriteItem) => {
597
+ if (!hasFullData) return;
636
598
  // If it has an href and no children, just navigate to the URL
637
599
  const node = resolveRef(favorite.nodeKey);
638
600
  if (favorite.href && (!node?.pages || node.pages.length === 0)) {
@@ -663,6 +625,11 @@ export default function NestedSideBar() {
663
625
 
664
626
  const isTopLevel = navigationStack.length === 1;
665
627
 
628
+ // The list drilled into (e.g. Channels): its items, even those in groups under it (e.g. ADRs
629
+ // grouped by status), drop a default icon that repeats the list's own
630
+ const levelNode = currentLevel.key ? resolveRef(currentLevel.key) : null;
631
+ const repeatsLevelIcon = (item: NavNode) => !!levelNode && hidesDefaultItemIcon(levelNode, item, false);
632
+
666
633
  /**
667
634
  * Render a list of child refs (resolving keys as needed)
668
635
  */
@@ -674,7 +641,7 @@ export default function NestedSideBar() {
674
641
  if (currentItemGroup.length > 0) {
675
642
  result.push(
676
643
  <div key={`items-${result.length}`} className="flex flex-col gap-0.5 mb-1.5">
677
- {currentItemGroup.map((item, idx) => renderItem(item.node, item.key, idx))}
644
+ {currentItemGroup.map((item, idx) => renderItem(item.node, item.key, idx, repeatsLevelIcon(item.node)))}
678
645
  </div>
679
646
  );
680
647
  currentItemGroup = [];
@@ -795,9 +762,12 @@ export default function NestedSideBar() {
795
762
  return renderGroup(child, childKey, childIndex, depth + 1);
796
763
  }
797
764
 
798
- // Under a nested group the header already conveys the resource type, so the
799
- // default per-collection glyph is dropped; custom icons still show.
800
- return renderItem(child, childKey, childIndex, isNested);
765
+ return renderItem(
766
+ child,
767
+ childKey,
768
+ childIndex,
769
+ hidesDefaultItemIcon(group, child, isNested) || repeatsLevelIcon(child)
770
+ );
801
771
  })}
802
772
  </div>
803
773
  )}
@@ -814,9 +784,9 @@ export default function NestedSideBar() {
814
784
  const isFav = isFavorited(itemKey);
815
785
  const canFavorite = itemKey !== null; // Only items with keys can be favorited
816
786
 
817
- // Get icon component from lucide-react. When suppressDefaultIcon is set (e.g. items
818
- // listed under a typed subtle subgroup) the default per-collection glyph is hidden;
819
- // a custom icon (item.leftIcon) is always still shown.
787
+ // Get icon component from lucide-react. When suppressDefaultIcon is set (the group heading
788
+ // already shows the item's type, see hidesDefaultItemIcon) the default per-collection glyph
789
+ // is hidden; a custom icon (item.leftIcon) is always still shown.
820
790
  const IconComponent =
821
791
  item.icon && !suppressDefaultIcon ? (LucideIcons as unknown as Record<string, LucideIcons.LucideIcon>)[item.icon] : null;
822
792
  const handleStarClick = (e: React.MouseEvent) => {
@@ -107,6 +107,8 @@ export const saveCollapsedSections = (preferences: SectionCollapsePreferences):
107
107
  };
108
108
 
109
109
  export const loadCollapsedSections = (): SectionCollapsePreferences => {
110
+ // Rendered on the server for the first paint, where there is nothing saved yet
111
+ if (typeof localStorage === 'undefined') return createSectionCollapsePreferences();
110
112
  try {
111
113
  const stored = localStorage.getItem(SECTION_PREFERENCES_KEY);
112
114
  if (stored) {