@eventcatalog/core 5.0.0-beta.12 → 5.0.0-beta.14

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 (90) hide show
  1. package/LICENSE +13 -12
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/catalog-runtime.cjs +1 -1
  7. package/dist/{chunk-TSY5CC27.js → chunk-4KE3EUDJ.js} +1 -1
  8. package/dist/{chunk-FY6MSP6J.js → chunk-I3XDOICA.js} +1 -1
  9. package/dist/{chunk-QB7X34OD.js → chunk-OFK7U7BX.js} +1 -1
  10. package/dist/{chunk-CX2VB7AI.js → chunk-PI5YNZZZ.js} +1 -1
  11. package/dist/{chunk-BWC4YKW3.js → chunk-WY6M3RPT.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/eventcatalog.cjs +11 -6
  15. package/dist/eventcatalog.js +14 -9
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  19. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  20. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  21. package/dist/runtime-dependencies/manifest.json +3 -0
  22. package/dist/utils/cli-logger.cjs +1 -1
  23. package/dist/utils/cli-logger.js +2 -2
  24. package/eventcatalog/astro.config.mjs +13 -1
  25. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  26. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  27. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  28. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +11 -3
  29. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
  33. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  34. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
  35. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
  36. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
  37. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +133 -71
  38. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +66 -66
  39. package/eventcatalog/src/features/studio/agent-choreography.ts +5 -4
  40. package/eventcatalog/src/features/studio/agents.ts +28 -0
  41. package/eventcatalog/src/features/studio/api/canvases-api.ts +1 -1
  42. package/eventcatalog/src/features/studio/canvas-actions.ts +22 -14
  43. package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
  44. package/eventcatalog/src/features/studio/catalog.ts +1 -4
  45. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +246 -115
  46. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +7 -3
  47. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
  48. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
  49. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +1 -1
  50. package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
  51. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +102 -241
  52. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  53. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +1 -1
  54. package/eventcatalog/src/features/studio/components/Presence.tsx +6 -8
  55. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
  56. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  57. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1005 -354
  58. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  59. package/eventcatalog/src/features/studio/from-visualiser.ts +1 -1
  60. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
  61. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
  62. package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
  63. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +162 -23
  64. package/eventcatalog/src/features/studio/identity.ts +23 -1
  65. package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
  66. package/eventcatalog/src/features/studio/pages/index.astro +1 -2
  67. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +7 -22
  68. package/eventcatalog/src/features/studio/server/runtime.ts +4 -5
  69. package/package.json +3 -3
  70. package/eventcatalog/src/features/studio/canvas-doc.ts +0 -569
  71. package/eventcatalog/src/features/studio/clipboard.ts +0 -97
  72. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -119
  73. package/eventcatalog/src/features/studio/components/Comments.tsx +0 -777
  74. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
  75. package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
  76. package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
  77. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
  78. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -271
  79. package/eventcatalog/src/features/studio/components/icons.ts +0 -53
  80. package/eventcatalog/src/features/studio/components/status.ts +0 -53
  81. package/eventcatalog/src/features/studio/edges.ts +0 -56
  82. package/eventcatalog/src/features/studio/flow-state.ts +0 -121
  83. package/eventcatalog/src/features/studio/grouping.ts +0 -230
  84. package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +0 -20
  85. package/eventcatalog/src/features/studio/layout.ts +0 -56
  86. package/eventcatalog/src/features/studio/level-layout.ts +0 -145
  87. package/eventcatalog/src/features/studio/levels.ts +0 -337
  88. package/eventcatalog/src/features/studio/node-types.ts +0 -245
  89. package/eventcatalog/src/features/studio/placement.ts +0 -197
  90. package/eventcatalog/src/features/studio/studio.css +0 -273
@@ -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>
@@ -21,57 +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,
28
29
  hidesDefaultItemIcon,
29
30
  isGroupCollapsed,
31
+ resolveNavRef,
32
+ rootLevel,
33
+ stripBasePath,
30
34
  toggleGroupCollapsed,
35
+ type NavigationLevel,
36
+ type SidebarSnapshot,
31
37
  } from './utils';
32
38
  import { resolveIconUrl } from '@utils/icon';
33
39
 
34
40
  const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
35
41
 
36
- /**
37
- * Strip the configured base path from a pathname so URL pattern matching works
38
- * regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
39
- */
40
- const stripBasePath = (pathname: string): string => {
41
- const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
42
- if (base && pathname.startsWith(base)) {
43
- return pathname.slice(base.length) || '/';
44
- }
45
- return pathname;
46
- };
47
-
48
42
  // ============================================
49
43
  // Component
50
44
  // ============================================
51
45
 
52
- type NavigationLevel = {
53
- key: string | null; // The key of the node that was drilled into (null for root)
54
- entries: ChildRef[];
55
- title: string;
56
- 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;
57
52
  };
58
53
 
59
- export default function NestedSideBar() {
60
- const data = useStore(sidebarStore);
54
+ export default function NestedSideBar({ snapshot }: NestedSideBarProps) {
55
+ const storeData = useStore(sidebarStore);
61
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;
62
61
 
63
62
  // Guard against undefined data (e.g., during hydration)
64
63
  // Use useMemo to ensure stable references for roots and nodes
65
64
  const roots = useMemo(() => data?.roots ?? [], [data?.roots]);
66
65
  const nodes = useMemo(() => data?.nodes ?? {}, [data?.nodes]);
67
66
 
68
- const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(() => [
69
- { key: null, entries: [], title: 'Documentation' },
70
- ]);
67
+ const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(
68
+ () => snapshot?.stack ?? [{ key: null, entries: [], title: 'Documentation' }]
69
+ );
71
70
  const [animationKey, setAnimationKey] = useState(0);
72
71
  const [slideDirection, setSlideDirection] = useState<'forward' | 'backward' | null>(null);
73
72
  const [isInitialized, setIsInitialized] = useState(false);
74
- const [currentPath, setCurrentPath] = useState<string>('');
73
+ const [currentPath, setCurrentPath] = useState<string>(snapshot?.url ?? '');
75
74
  const [sectionCollapsePreferences, setSectionCollapsePreferences] = useState(loadCollapsedSections);
76
75
  const [showPathPreview, setShowPathPreview] = useState(false);
77
76
  const [showFullPath, setShowFullPath] = useState(false);
@@ -79,33 +78,7 @@ export default function NestedSideBar() {
79
78
 
80
79
  // Build a lookup map for faster URL navigation
81
80
  // Map format: "type:id" -> "nodeKey"
82
- const nodeLookup = useMemo(() => {
83
- const lookup = new Map<string, string>();
84
-
85
- Object.entries(nodes).forEach(([key, value]) => {
86
- // Skip keys that are references (string values pointing to other keys)
87
- // These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
88
- if (typeof value === 'string') return;
89
-
90
- // Key formats:
91
- // - "type:id:version" (e.g., "service:OrdersService:0.0.3")
92
- // - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
93
- // - "list:name" (e.g., "list:domains") - skip these
94
- const parts = key.split(':');
95
-
96
- // Skip list items
97
- if (parts[0] === 'list') return;
98
-
99
- if (parts.length >= 2) {
100
- // Store as "type:id"
101
- const type = parts[0];
102
- const id = parts[1];
103
- lookup.set(`${type}:${id}`, key);
104
- }
105
- });
106
-
107
- return lookup;
108
- }, [nodes]);
81
+ const nodeLookup = useMemo(() => buildNodeLookup(nodes), [nodes]);
109
82
 
110
83
  /**
111
84
  * Toggle section collapse state
@@ -126,7 +99,7 @@ export default function NestedSideBar() {
126
99
  setNavigationStack((prevStack) => {
127
100
  // Only update if the current stack has no entries (initial state)
128
101
  if (prevStack.length === 1 && prevStack[0].entries.length === 0) {
129
- return [{ key: null, entries: roots, title: 'Documentation' }];
102
+ return [rootLevel(roots)];
130
103
  }
131
104
  return prevStack;
132
105
  });
@@ -146,21 +119,7 @@ export default function NestedSideBar() {
146
119
  * Resolve a child reference to a NavNode
147
120
  * Handles both direct keys and string references (unversioned aliases pointing to versioned keys)
148
121
  */
149
- const resolveRef = useCallback(
150
- (ref: ChildRef): NavNode | null => {
151
- if (typeof ref === 'string') {
152
- const node = nodes[ref];
153
- if (!node) return null;
154
- // If node is a string, it's a reference to another key (e.g., unversioned alias)
155
- if (typeof node === 'string') {
156
- return (nodes[node] as NavNode) ?? null;
157
- }
158
- return node;
159
- }
160
- return ref;
161
- },
162
- [nodes]
163
- );
122
+ const resolveRef = useCallback((ref: ChildRef): NavNode | null => resolveNavRef(nodes, ref), [nodes]);
164
123
 
165
124
  /**
166
125
  * Check if a node is visible (default: true)
@@ -175,7 +134,7 @@ export default function NestedSideBar() {
175
134
  */
176
135
  const buildStackFromPath = useCallback(
177
136
  (path: string[]): NavigationLevel[] => {
178
- const stack: NavigationLevel[] = [{ key: null, entries: roots, title: 'Documentation' }];
137
+ const stack: NavigationLevel[] = [rootLevel(roots)];
179
138
 
180
139
  for (const key of path) {
181
140
  const node = resolveRef(key);
@@ -281,7 +240,7 @@ export default function NestedSideBar() {
281
240
  if (foundNode && foundNode.pages && foundNode.pages.length > 0) {
282
241
  // Fallback: Flattened navigation
283
242
  return [
284
- { key: null, entries: roots, title: 'Documentation' },
243
+ rootLevel(roots),
285
244
  { key: foundNodeKey, entries: foundNode.pages, title: foundNode.title, badge: foundNode.badge },
286
245
  ];
287
246
  }
@@ -296,7 +255,7 @@ export default function NestedSideBar() {
296
255
  setSlideDirection('backward');
297
256
  setAnimationKey((prev) => prev + 1);
298
257
  }
299
- return [{ key: null, entries: roots, title: 'Documentation' }];
258
+ return [rootLevel(roots)];
300
259
  });
301
260
  return true;
302
261
  }
@@ -309,14 +268,14 @@ export default function NestedSideBar() {
309
268
  * Restore state from localStorage on mount, or navigate to URL
310
269
  */
311
270
  useEffect(() => {
312
- if (!data || roots.length === 0 || isInitialized) return;
271
+ if (!hasFullData || roots.length === 0 || isInitialized) return;
313
272
 
314
273
  const currentUrl = window.location.pathname;
315
274
  const normalizedCurrentUrl = stripBasePath(currentUrl);
316
275
 
317
276
  // Force root navigation on homepage
318
277
  if (normalizedCurrentUrl === '/' || normalizedCurrentUrl === '') {
319
- setNavigationStack([{ key: null, entries: roots, title: 'Documentation' }]);
278
+ setNavigationStack([rootLevel(roots)]);
320
279
  setIsInitialized(true);
321
280
  return;
322
281
  }
@@ -347,7 +306,7 @@ export default function NestedSideBar() {
347
306
  const targetNode = resolveRef(targetKey);
348
307
  if (targetNode && targetNode.pages && targetNode.pages.length > 0) {
349
308
  finalStack = [
350
- { key: null, entries: roots, title: 'Documentation' },
309
+ rootLevel(roots),
351
310
  { key: targetKey, entries: targetNode.pages, title: targetNode.title, badge: targetNode.badge },
352
311
  ];
353
312
  }
@@ -355,13 +314,13 @@ export default function NestedSideBar() {
355
314
 
356
315
  // 3. Fallback to root
357
316
  if (!finalStack) {
358
- setNavigationStack([{ key: null, entries: roots, title: 'Documentation' }]);
317
+ setNavigationStack([rootLevel(roots)]);
359
318
  } else {
360
319
  setNavigationStack(finalStack);
361
320
  }
362
321
 
363
322
  setIsInitialized(true);
364
- }, [data, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
323
+ }, [hasFullData, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
365
324
 
366
325
  /**
367
326
  * Save state whenever navigation changes
@@ -575,6 +534,7 @@ export default function NestedSideBar() {
575
534
  * Handle drilling down into an item with children
576
535
  */
577
536
  const handleDrillDown = (node: NavNode, nodeKey: string | null) => {
537
+ if (!hasFullData) return;
578
538
  if (node.pages && node.pages.length > 0) {
579
539
  setSlideDirection('forward');
580
540
  setAnimationKey((prev) => prev + 1);
@@ -590,7 +550,7 @@ export default function NestedSideBar() {
590
550
  * Navigate back one level
591
551
  */
592
552
  const navigateBack = () => {
593
- if (navigationStack.length > 1) {
553
+ if (hasFullData && navigationStack.length > 1) {
594
554
  setSlideDirection('backward');
595
555
  setAnimationKey((prev) => prev + 1);
596
556
  setNavigationStack(navigationStack.slice(0, -1));
@@ -604,7 +564,7 @@ export default function NestedSideBar() {
604
564
  * Navigate to a specific level in the stack
605
565
  */
606
566
  const navigateToLevel = (levelIndex: number) => {
607
- if (levelIndex < navigationStack.length - 1) {
567
+ if (hasFullData && levelIndex < navigationStack.length - 1) {
608
568
  setSlideDirection('backward');
609
569
  setAnimationKey((prev) => prev + 1);
610
570
  setNavigationStack(navigationStack.slice(0, levelIndex + 1));
@@ -634,6 +594,7 @@ export default function NestedSideBar() {
634
594
  * Navigate to a favorited item
635
595
  */
636
596
  const navigateToFavorite = (favorite: FavoriteItem) => {
597
+ if (!hasFullData) return;
637
598
  // If it has an href and no children, just navigate to the URL
638
599
  const node = resolveRef(favorite.nodeKey);
639
600
  if (favorite.href && (!node?.pages || node.pages.length === 0)) {
@@ -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) {
@@ -1,9 +1,130 @@
1
1
  // Shared utilities for NestedSideBar components
2
2
 
3
- import type { NavNode } from '@stores/sidebar-store/state';
3
+ import type { ChildRef, NavigationData, NavNode } from '@stores/sidebar-store/state';
4
4
 
5
5
  export const SIDEBAR_GROUP_COLLAPSE_THRESHOLD = 5;
6
6
 
7
+ /**
8
+ * Strip the configured base path from a pathname so URL pattern matching works
9
+ * regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
10
+ */
11
+ export const stripBasePath = (pathname: string): string => {
12
+ const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
13
+ if (base && pathname.startsWith(base)) {
14
+ return pathname.slice(base.length) || '/';
15
+ }
16
+ return pathname;
17
+ };
18
+
19
+ export type NavigationLevel = {
20
+ key: string | null; // The key of the node that was drilled into (null for root)
21
+ entries: ChildRef[];
22
+ title: string;
23
+ badge?: string; // Category badge (e.g., "Domain", "Service")
24
+ };
25
+
26
+ export const rootLevel = (roots: ChildRef[]): NavigationLevel => ({ key: null, entries: roots, title: 'Documentation' });
27
+
28
+ /** Resolve a child reference to a node, following unversioned aliases (string → key). */
29
+ export const resolveNavRef = (nodes: NavigationData['nodes'], ref: ChildRef): NavNode | null => {
30
+ if (typeof ref !== 'string') return ref;
31
+ const node = nodes[ref];
32
+ if (!node) return null;
33
+ if (typeof node === 'string') return (nodes[node] as NavNode) ?? null;
34
+ return node;
35
+ };
36
+
37
+ /** Map of "type:id" → versioned node key, used to match URLs to nodes. */
38
+ export const buildNodeLookup = (nodes: NavigationData['nodes']): Map<string, string> => {
39
+ const lookup = new Map<string, string>();
40
+
41
+ Object.entries(nodes).forEach(([key, value]) => {
42
+ // Skip keys that are references (string values pointing to other keys)
43
+ // These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
44
+ if (typeof value === 'string') return;
45
+
46
+ // Key formats:
47
+ // - "type:id:version" (e.g., "service:OrdersService:0.0.3")
48
+ // - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
49
+ // - "list:name" (e.g., "list:domains") - skip these
50
+ const parts = key.split(':');
51
+
52
+ // Skip list items
53
+ if (parts[0] === 'list') return;
54
+
55
+ if (parts.length >= 2) {
56
+ // Store as "type:id"
57
+ lookup.set(`${parts[0]}:${parts[1]}`, key);
58
+ }
59
+ });
60
+
61
+ return lookup;
62
+ };
63
+
64
+ /**
65
+ * What the sidebar shows for a URL before it knows the reader's saved drill-down path:
66
+ * the root level on the homepage or for pages without children, otherwise the matching
67
+ * resource's own level (the same "flattened" stack the client falls back to).
68
+ */
69
+ export const getStackForUrl = (
70
+ url: string,
71
+ data: NavigationData,
72
+ nodeLookup: ReadonlyMap<string, string> = buildNodeLookup(data.nodes)
73
+ ): NavigationLevel[] => {
74
+ const root = rootLevel(data.roots);
75
+ if (url === '/' || url === '') return [root];
76
+
77
+ const targetKey = findNodeKeyByUrl(url, data.nodes, nodeLookup);
78
+ const target = targetKey ? resolveNavRef(data.nodes, targetKey) : null;
79
+ if (!targetKey || !target?.pages?.length) return [root];
80
+
81
+ return [root, { key: targetKey, entries: target.pages, title: target.title, badge: target.badge }];
82
+ };
83
+
84
+ /**
85
+ * Everything the sidebar needs to render one page's level, without the whole catalog: the
86
+ * stack for the URL and only the nodes that level shows (its items, and the contents of its
87
+ * groups). Rendered on the server so the sidebar appears with the HTML, and handed to the
88
+ * island so it keeps showing it until the full navigation data loads.
89
+ */
90
+ export type SidebarSnapshot = {
91
+ url: string;
92
+ stack: NavigationLevel[];
93
+ data: NavigationData;
94
+ };
95
+
96
+ const nodeLookupCache = new WeakMap<NavigationData, Map<string, string>>();
97
+
98
+ export const buildSidebarSnapshot = (data: NavigationData, url: string): SidebarSnapshot => {
99
+ let nodeLookup = nodeLookupCache.get(data);
100
+ if (!nodeLookup) {
101
+ nodeLookup = buildNodeLookup(data.nodes);
102
+ nodeLookupCache.set(data, nodeLookup);
103
+ }
104
+
105
+ const stack = getStackForUrl(url, data, nodeLookup);
106
+ const level = stack[stack.length - 1];
107
+ const nodes: NavigationData['nodes'] = {};
108
+
109
+ const include = (ref: ChildRef) => {
110
+ if (typeof ref === 'string') {
111
+ if (ref in nodes) return;
112
+ const value = data.nodes[ref];
113
+ if (value === undefined) return;
114
+ nodes[ref] = value;
115
+ if (typeof value === 'string' && data.nodes[value] !== undefined) nodes[value] = data.nodes[value];
116
+ }
117
+ const node = resolveNavRef(data.nodes, ref);
118
+ // Groups render their children inline; items with children only need their own node
119
+ if (node?.type === 'group') node.pages?.forEach(include);
120
+ };
121
+
122
+ if (level.key) include(level.key);
123
+ level.entries.forEach(include);
124
+
125
+ return { url, stack, data: { roots: data.roots, nodes } };
126
+ };
127
+
7
128
  /**
8
129
  * The user's overrides of each group's default collapse state, keyed by group id.
9
130
  * A group is in at most one set; absence means "use the group's default".
@@ -3,10 +3,18 @@ import type { HTMLAttributes } from 'astro/types';
3
3
 
4
4
  interface Props extends Omit<HTMLAttributes<'div'>, 'children'> {}
5
5
  import NestedSideBar from './NestedSideBar';
6
+ import { buildSidebarSnapshot, stripBasePath } from './NestedSideBar/utils';
7
+ import { getNestedSideBarData } from '@stores/sidebar-store/state';
6
8
  import { ClientRouter } from 'astro:transitions';
9
+
10
+ // Render this page's sidebar level into the HTML so it shows before any JavaScript runs;
11
+ // the full navigation still loads from /api/sidebar-data.json (see SearchDataLoader)
12
+ const snapshot = buildSidebarSnapshot(await getNestedSideBarData(), stripBasePath(Astro.url.pathname));
7
13
  ---
8
14
 
9
15
  <div {...Astro.props}>
10
16
  <ClientRouter />
11
- <NestedSideBar client:only="react" />
17
+ <NestedSideBar client:only="react" snapshot={snapshot}>
18
+ <NestedSideBar slot="fallback" snapshot={snapshot} />
19
+ </NestedSideBar>
12
20
  </div>
@@ -1,6 +1,6 @@
1
1
  // Shared by the canvas MCP App (client) and the MCP server: no server-only imports here
2
2
  import type { CatalogRelation, CatalogResource } from '../../../studio/catalog-resources';
3
- import type { CanvasStatus } from '../../../studio/canvas-doc';
3
+ import { type CanvasStatus } from '@eventcatalog/visualiser/studio';
4
4
 
5
5
  export const CANVAS_RESOURCE_URI = 'ui://eventcatalog/canvas.html';
6
6
  /** Key of the payload in openCanvas results' `_meta` */
@@ -40,8 +40,8 @@ export type CanvasView = {
40
40
  canvasUrl: string;
41
41
  /** The EventCatalog MCP server, for "Connect your agent" */
42
42
  mcpUrl: string;
43
- /** What to call the user on the canvas, when the agent knows their name */
44
- userName?: string;
43
+ /** The chat's agent: the view joins the canvas as it (its name, colour and logo) */
44
+ agent?: { name: string; color: string };
45
45
  };
46
46
 
47
47
  export type CanvasCatalog = { resources: CatalogResource[]; relations: CatalogRelation[] };
@@ -4,10 +4,12 @@
4
4
  */
5
5
  @import 'tailwindcss' source(none);
6
6
  @import '../../../../styles/theme.css';
7
- @import '../../../studio/studio.css';
7
+ @import '@eventcatalog/visualiser/studio.css';
8
8
 
9
9
  @source './*.tsx';
10
10
  @source '../../../studio/components/*.{ts,tsx}';
11
+ /* Studio's shared canvas (nodes, comments, hotspots), from the visualiser in this repo */
12
+ @source '../../../../../../../visualiser/src/studio';
11
13
 
12
14
  body {
13
15
  margin: 0;
@@ -15,7 +15,8 @@ import type { App } from '@modelcontextprotocol/ext-apps';
15
15
  import styles from './styles.css?inline';
16
16
  import visualiserStyles from '@eventcatalog/visualiser/styles.css?inline';
17
17
  import StudioDesigner, { type SelectedNode } from '@features/studio/components/StudioDesigner';
18
- import { CANVAS_STATUS_LOOK, STATUS } from '@features/studio/components/status';
18
+ import { getAgent } from '@features/studio/agents';
19
+ import { CANVAS_STATUS_LOOK, STATUS } from '@eventcatalog/visualiser/studio';
19
20
  import { getToolResultPayload, mountView, useMcpAppView } from '../shared/app-view';
20
21
  import {
21
22
  CANVAS_CATALOG_TOOL,
@@ -212,10 +213,10 @@ function CanvasApp() {
212
213
  const iconButton =
213
214
  'flex items-center gap-1 rounded-md px-2 py-1 text-xs text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.5)]';
214
215
 
215
- // No join form inside a chat: use the name the agent passed, or name them after the chat they're in
216
+ // No join form inside a chat: it joins as the chat's agent (e.g. ChatGPT or Claude, with its logo), as the agent
217
+ // working on the canvas does, else as the chat it's in
216
218
  const hostName = app?.getHostVersion()?.name;
217
- const userName = (view: CanvasView) =>
218
- view.userName ?? `${hostName ? hostName.replace(/^\w/, (letter) => letter.toUpperCase()) : 'Chat'} user`;
219
+ const agentOf = (view: CanvasView) => view.agent ?? getAgent(undefined, hostName);
219
220
 
220
221
  return (
221
222
  <div className="flex flex-col" style={{ height: fullscreen ? '100vh' : INLINE_HEIGHT }}>
@@ -255,11 +256,12 @@ function CanvasApp() {
255
256
  shareUrl={canvas.canvasUrl}
256
257
  mcpUrl={canvas.mcpUrl}
257
258
  syncViaTools={syncViaTools}
258
- defaultName={userName(canvas)}
259
+ asAgent={agentOf(canvas)}
259
260
  resources={catalog.resources}
260
261
  relations={catalog.relations}
261
262
  onSelectionChange={setSelection}
262
263
  followChanges
264
+ paletteCollapsed
263
265
  />
264
266
  ) : (
265
267
  <CanvasPicker