@eventcatalog/core 3.5.2 → 3.6.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 (46) hide show
  1. package/README.md +1 -1
  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/{chunk-YVX5C6L3.js → chunk-FCIJEGOL.js} +1 -1
  7. package/dist/{chunk-WO3AKJVB.js → chunk-N2VBSHPU.js} +1 -1
  8. package/dist/{chunk-OKWCSRLE.js → chunk-OFHFRJ42.js} +1 -1
  9. package/dist/{chunk-YOFNY2RC.js → chunk-SI6IEUYS.js} +1 -1
  10. package/dist/{chunk-YTZSPYJN.js → chunk-XRLZZXIS.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +1 -1
  14. package/dist/eventcatalog.js +5 -5
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/eventcatalog/astro.config.mjs +2 -1
  20. package/eventcatalog/src/components/EnvironmentDropdown.tsx +1 -1
  21. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +477 -0
  22. package/eventcatalog/src/components/MDX/components.tsx +2 -0
  23. package/eventcatalog/src/components/Search/SearchDataLoader.astro +23 -11
  24. package/eventcatalog/src/components/Search/SearchModal.tsx +17 -2
  25. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +12 -6
  26. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -16
  27. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +816 -0
  28. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +161 -0
  29. package/eventcatalog/src/components/Tables/Discover/columns.tsx +565 -0
  30. package/eventcatalog/src/components/Tables/Discover/index.ts +4 -0
  31. package/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +1 -1
  32. package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +1 -1
  33. package/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +1 -1
  34. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +1 -1
  35. package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +54 -64
  36. package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +15 -30
  37. package/eventcatalog/src/enterprise/plans/index.astro +125 -98
  38. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +1 -1
  39. package/eventcatalog/src/pages/api/sidebar-data.json.ts +22 -0
  40. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -1
  41. package/eventcatalog/src/pages/discover/[type]/index.astro +360 -41
  42. package/eventcatalog/src/pages/docs/custom/feature.astro +45 -39
  43. package/eventcatalog/src/remark-plugins/resource-ref.ts +51 -0
  44. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -1
  45. package/eventcatalog/src/stores/sidebar-store/state.ts +25 -22
  46. package/package.json +3 -2
@@ -0,0 +1,477 @@
1
+ ---
2
+ /**
3
+ * Inline resource reference component with hover tooltip.
4
+ *
5
+ * Usage via remark plugin:
6
+ * [[service|OrderService]] -> Links to OrderService with tooltip
7
+ * [[Order]] -> Shorthand, defaults to entity type
8
+ */
9
+ import { buildUrl } from '@utils/url-builder';
10
+ import {
11
+ getItemsFromCollectionByIdAndSemverOrLatest,
12
+ resourceToCollectionMap,
13
+ getDeprecatedDetails,
14
+ } from '@utils/collections/util';
15
+ import { getCollection } from 'astro:content';
16
+ import { getServiceSpecifications, getSpecUrl, getSpecLabel } from '@components/Grids/specification-utils';
17
+
18
+ interface Props {
19
+ type:
20
+ | 'entity'
21
+ | 'service'
22
+ | 'event'
23
+ | 'command'
24
+ | 'query'
25
+ | 'domain'
26
+ | 'flow'
27
+ | 'channel'
28
+ | 'diagram'
29
+ | 'container'
30
+ | 'user'
31
+ | 'team';
32
+ version?: string;
33
+ }
34
+
35
+ const { type = 'entity', version } = Astro.props;
36
+
37
+ // Get resource ID from slot content
38
+ const slotContent = await Astro.slots.render('default');
39
+ const resourceId = slotContent.trim();
40
+
41
+ // Map type to collection name
42
+ const collection = resourceToCollectionMap[type as keyof typeof resourceToCollectionMap];
43
+
44
+ // Type-specific styling using CSS variables from theme.css
45
+ // Maps to --ec-badge-{type}-text variables for consistency
46
+ const typeStyles: Record<string, { borderColor: string; label: string }> = {
47
+ entity: { borderColor: 'var(--ec-badge-query-text)', label: 'Entity' }, // purple
48
+ service: { borderColor: 'var(--ec-badge-service-text)', label: 'Service' }, // pink
49
+ event: { borderColor: 'var(--ec-badge-event-text)', label: 'Event' }, // amber/orange
50
+ command: { borderColor: 'var(--ec-badge-command-text)', label: 'Command' }, // pink
51
+ query: { borderColor: 'var(--ec-badge-query-text)', label: 'Query' }, // purple
52
+ domain: { borderColor: 'var(--ec-badge-domain-text)', label: 'Domain' }, // yellow
53
+ flow: { borderColor: 'var(--ec-badge-design-text)', label: 'Flow' }, // teal
54
+ channel: { borderColor: 'var(--ec-badge-channel-text)', label: 'Channel' }, // indigo
55
+ diagram: { borderColor: 'var(--ec-badge-default-text)', label: 'Diagram' }, // gray
56
+ container: { borderColor: 'var(--ec-badge-default-text)', label: 'Container' }, // gray
57
+ user: { borderColor: 'var(--ec-badge-default-text)', label: 'User' }, // gray
58
+ team: { borderColor: 'var(--ec-badge-default-text)', label: 'Team' }, // gray
59
+ };
60
+
61
+ // SVG icons for each type (from heroicons outline)
62
+ const typeIcons: Record<string, string> = {
63
+ entity:
64
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />', // Box/cube
65
+ service:
66
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />', // Server
67
+ event:
68
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />', // Bolt
69
+ command:
70
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />', // ChatBubble
71
+ query:
72
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />', // MagnifyingGlass
73
+ domain:
74
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 7.125C2.25 6.504 2.754 6 3.375 6h6c.621 0 1.125.504 1.125 1.125v3.75c0 .621-.504 1.125-1.125 1.125h-6a1.125 1.125 0 01-1.125-1.125v-3.75zM14.25 8.625c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-8.25zM3.75 16.125c0-.621.504-1.125 1.125-1.125h5.25c.621 0 1.125.504 1.125 1.125v2.25c0 .621-.504 1.125-1.125 1.125h-5.25a1.125 1.125 0 01-1.125-1.125v-2.25z" />', // RectangleGroup
75
+ flow: '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />', // QueueList
76
+ channel:
77
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />', // ArrowsRightLeft
78
+ diagram:
79
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />', // Map
80
+ container:
81
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />', // Database
82
+ user: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />', // User
83
+ team: '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />', // UserGroup
84
+ };
85
+
86
+ const style = typeStyles[type] || typeStyles.entity;
87
+ const iconPath = typeIcons[type] || typeIcons.entity;
88
+
89
+ let resource: any = null;
90
+ let href = '#';
91
+ let hasError = false;
92
+ let errorMessage = '';
93
+
94
+ try {
95
+ if (!collection) {
96
+ throw new Error(`Unknown resource type: ${type}`);
97
+ }
98
+
99
+ const resourcesCollection = await getCollection(collection as any);
100
+ const resources = getItemsFromCollectionByIdAndSemverOrLatest(resourcesCollection, resourceId, version);
101
+
102
+ if (resources.length === 0) {
103
+ throw new Error(`Resource not found: ${resourceId}`);
104
+ }
105
+
106
+ resource = resources[0];
107
+ // Diagrams use /diagrams/ path, other resources use /docs/{collection}/
108
+ href =
109
+ type === 'diagram'
110
+ ? buildUrl(`/diagrams/${resourceId}/${resource.data.version}`)
111
+ : buildUrl(`/docs/${collection}/${resourceId}/${resource.data.version}`);
112
+ } catch (error) {
113
+ hasError = true;
114
+ errorMessage = error instanceof Error ? error.message : 'Unknown error';
115
+ }
116
+
117
+ // Truncate summary if too long
118
+ const maxSummaryLength = 120;
119
+ const summary = resource?.data?.summary || '';
120
+ const truncatedSummary = summary.length > maxSummaryLength ? summary.slice(0, maxSummaryLength) + '...' : summary;
121
+
122
+ // Only these types have visualizers
123
+ const hasVisualizer = ['domain', 'service', 'event', 'query', 'command', 'container'].includes(type);
124
+
125
+ // Check deprecation status
126
+ const deprecation = resource ? getDeprecatedDetails(resource) : null;
127
+ const isDeprecated = deprecation?.isMarkedAsDeprecated || false;
128
+
129
+ // Get owners (first 2)
130
+ const owners = resource?.data?.owners?.slice(0, 2) || [];
131
+
132
+ // Check if message type has a schema
133
+ const isMessageType = ['event', 'command', 'query'].includes(type);
134
+ const hasSchema = isMessageType && resource?.data?.schemaPath;
135
+
136
+ // Check if resource has a repository URL
137
+ const repositoryUrl = resource?.data?.repository?.url;
138
+
139
+ // For services: get messages (sends/receives) with resolved versions
140
+ const maxMessages = 3;
141
+ const sends = resource?.data?.sends || [];
142
+ const receives = resource?.data?.receives || [];
143
+ const isService = type === 'service';
144
+
145
+ // Helper to resolve message version and collection - use specified version or fetch from collection
146
+ const resolveMessage = async (msg: any): Promise<{ version: string | null; collection: string | null }> => {
147
+ // If version is specified and not "latest", use it (assume event as default collection)
148
+ if (msg.version && msg.version !== 'latest') {
149
+ return { version: msg.version, collection: 'events' };
150
+ }
151
+
152
+ // Try to find the message in events, commands, or queries collections
153
+ const collections = ['events', 'commands', 'queries'];
154
+ for (const col of collections) {
155
+ try {
156
+ const items = await getCollection(col as any);
157
+ const found = getItemsFromCollectionByIdAndSemverOrLatest(items, msg.id);
158
+ if (found.length > 0 && found[0].data.version && found[0].data.version !== 'latest') {
159
+ return { version: found[0].data.version, collection: col };
160
+ }
161
+ } catch (e) {
162
+ // Collection might not exist or item not found, continue
163
+ }
164
+ }
165
+ return { version: null, collection: null };
166
+ };
167
+
168
+ // Resolve versions and collections for messages to show
169
+ const sendsWithVersions = await Promise.all(
170
+ sends.slice(0, maxMessages).map(async (msg: any) => {
171
+ const resolved = await resolveMessage(msg);
172
+ return { ...msg, resolvedVersion: resolved.version, resolvedCollection: resolved.collection };
173
+ })
174
+ );
175
+ const receivesWithVersions = await Promise.all(
176
+ receives.slice(0, maxMessages).map(async (msg: any) => {
177
+ const resolved = await resolveMessage(msg);
178
+ return { ...msg, resolvedVersion: resolved.version, resolvedCollection: resolved.collection };
179
+ })
180
+ );
181
+
182
+ // Get specifications for services
183
+ const specifications = isService ? getServiceSpecifications(resource?.data) : [];
184
+ const hasSpecifications = specifications.length > 0;
185
+
186
+ // Generate unique ID for this instance
187
+ const tooltipId = `ref-tooltip-${Math.random().toString(36).slice(2, 9)}`;
188
+ ---
189
+
190
+ <span class="resource-ref-wrapper inline">
191
+ {
192
+ hasError ? (
193
+ <span
194
+ class="text-[rgb(var(--ec-page-text-muted))] underline decoration-wavy decoration-red-400/50 underline-offset-2 cursor-help"
195
+ title={`Resource not found: ${resourceId}`}
196
+ >
197
+ {resourceId}
198
+ </span>
199
+ ) : (
200
+ <>
201
+ <a
202
+ href={href}
203
+ class:list={[
204
+ 'resource-ref-trigger underline decoration-dotted hover:decoration-solid underline-offset-2 font-medium transition-colors',
205
+ isDeprecated
206
+ ? 'text-amber-600 decoration-amber-500/50'
207
+ : 'text-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent-hover))]',
208
+ ]}
209
+ data-tooltip-id={tooltipId}
210
+ >
211
+ {resource?.data?.name || resourceId}
212
+ {isDeprecated && ' (deprecated)'}
213
+ </a>
214
+ <span
215
+ id={tooltipId}
216
+ class="resource-ref-tooltip fixed w-80 bg-[rgb(var(--ec-card-bg))] border border-[rgb(var(--ec-page-border))] border-l-[3px] rounded-r-lg rounded-l-none shadow-lg shadow-black/8 z-[9999] text-left overflow-hidden opacity-0 pointer-events-none transition-all duration-150 scale-95 origin-top-left"
217
+ style={`border-left-color: rgb(${style.borderColor});`}
218
+ >
219
+ {/* Deprecation banner */}
220
+ {isDeprecated && (
221
+ <span class="flex items-center gap-1.5 px-3 py-1.5 bg-[rgb(var(--ec-badge-event-bg))] text-[rgb(var(--ec-badge-event-text))] text-xs font-medium">
222
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
223
+ <path
224
+ stroke-linecap="round"
225
+ stroke-linejoin="round"
226
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
227
+ />
228
+ </svg>
229
+ Deprecated
230
+ </span>
231
+ )}
232
+
233
+ <span class="block p-3">
234
+ {/* Header: Name with icon top-right */}
235
+ <span class="flex justify-between items-start mb-2">
236
+ <span class="flex flex-col">
237
+ <span class="flex items-baseline gap-2">
238
+ <span class="font-semibold text-[rgb(var(--ec-page-text))] text-[0.95rem] leading-tight">
239
+ {resource?.data?.name || resourceId}
240
+ </span>
241
+ <span class="text-[0.65rem] text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wide">{style.label}</span>
242
+ </span>
243
+ <span class="text-[0.7rem] font-mono text-[rgb(var(--ec-page-text-muted))] mt-0.5">
244
+ v{resource?.data?.version}
245
+ </span>
246
+ </span>
247
+ <svg
248
+ class="w-5 h-5 flex-shrink-0"
249
+ style={`color: rgb(${style.borderColor});`}
250
+ fill="none"
251
+ viewBox="0 0 24 24"
252
+ stroke="currentColor"
253
+ stroke-width="1.5"
254
+ set:html={iconPath}
255
+ />
256
+ </span>
257
+
258
+ {/* Summary */}
259
+ {truncatedSummary && (
260
+ <span class="block text-[rgb(var(--ec-page-text-muted))] text-[0.8rem] leading-relaxed mb-3">
261
+ {truncatedSummary}
262
+ </span>
263
+ )}
264
+
265
+ {/* Metadata */}
266
+ {(isService && (sends.length > 0 || receives.length > 0)) || hasSpecifications || owners.length > 0 ? (
267
+ <span class="block text-[0.7rem] mb-3 space-y-1">
268
+ {isService && sends.length > 0 && (
269
+ <span class="flex items-baseline gap-2">
270
+ <span class="text-[rgb(var(--ec-page-text-muted))]">Publishes</span>
271
+ <span class="font-mono text-[rgb(var(--ec-page-text))]">
272
+ {sendsWithVersions.slice(0, 2).map((msg: any, idx: number) => (
273
+ <>
274
+ {msg.resolvedVersion && msg.resolvedCollection ? (
275
+ <a
276
+ href={buildUrl(`/docs/${msg.resolvedCollection}/${msg.id}/${msg.resolvedVersion}`)}
277
+ class="hover:underline hover:text-[rgb(var(--ec-accent))]"
278
+ >
279
+ {msg.id}
280
+ </a>
281
+ ) : (
282
+ <span class="text-[rgb(var(--ec-page-text-muted))]">{msg.id}</span>
283
+ )}
284
+ {idx < Math.min(sendsWithVersions.length, 2) - 1 && ', '}
285
+ </>
286
+ ))}
287
+ {sends.length > 2 && <span class="text-[rgb(var(--ec-page-text-muted))]"> +{sends.length - 2}</span>}
288
+ </span>
289
+ </span>
290
+ )}
291
+ {isService && receives.length > 0 && (
292
+ <span class="flex items-baseline gap-2">
293
+ <span class="text-[rgb(var(--ec-page-text-muted))]">Subscribes</span>
294
+ <span class="font-mono text-[rgb(var(--ec-page-text))]">
295
+ {receivesWithVersions.slice(0, 2).map((msg: any, idx: number) => (
296
+ <>
297
+ {msg.resolvedVersion && msg.resolvedCollection ? (
298
+ <a
299
+ href={buildUrl(`/docs/${msg.resolvedCollection}/${msg.id}/${msg.resolvedVersion}`)}
300
+ class="hover:underline hover:text-[rgb(var(--ec-accent))]"
301
+ >
302
+ {msg.id}
303
+ </a>
304
+ ) : (
305
+ <span class="text-[rgb(var(--ec-page-text-muted))]">{msg.id}</span>
306
+ )}
307
+ {idx < Math.min(receivesWithVersions.length, 2) - 1 && ', '}
308
+ </>
309
+ ))}
310
+ {receives.length > 2 && <span class="text-[rgb(var(--ec-page-text-muted))]"> +{receives.length - 2}</span>}
311
+ </span>
312
+ </span>
313
+ )}
314
+ {hasSpecifications && (
315
+ <span class="flex items-baseline gap-2">
316
+ <span class="text-[rgb(var(--ec-page-text-muted))]">APIs</span>
317
+ <span class="font-mono text-[rgb(var(--ec-page-text))]">
318
+ {specifications.map((spec: any, idx: number) => (
319
+ <>
320
+ <a
321
+ href={getSpecUrl(spec, resourceId, resource?.data?.version)}
322
+ class="hover:underline hover:text-[rgb(var(--ec-accent))]"
323
+ >
324
+ {getSpecLabel(spec.type)}
325
+ </a>
326
+ {idx < specifications.length - 1 && ', '}
327
+ </>
328
+ ))}
329
+ </span>
330
+ </span>
331
+ )}
332
+ {owners.length > 0 && (
333
+ <span class="flex items-baseline gap-2">
334
+ <span class="text-[rgb(var(--ec-page-text-muted))]">Owner</span>
335
+ <span class="font-mono text-[rgb(var(--ec-page-text))]">
336
+ {owners.map((o: any, idx: number) => {
337
+ const ownerId = typeof o === 'string' ? o : o.id;
338
+ return (
339
+ <>
340
+ <a
341
+ href={buildUrl(`/docs/users/${ownerId}`)}
342
+ class="hover:underline hover:text-[rgb(var(--ec-accent))]"
343
+ >
344
+ {ownerId}
345
+ </a>
346
+ {idx < owners.length - 1 && ', '}
347
+ </>
348
+ );
349
+ })}
350
+ </span>
351
+ </span>
352
+ )}
353
+ </span>
354
+ ) : null}
355
+
356
+ {/* Actions */}
357
+ <span class="flex items-center justify-between pt-2 border-t border-[rgb(var(--ec-page-border))] text-[0.7rem]">
358
+ <span class="flex items-center gap-3">
359
+ {type === 'flow' && (
360
+ <a
361
+ href={buildUrl(`/visualiser/${collection}/${resourceId}/${resource?.data?.version}`)}
362
+ class="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
363
+ >
364
+ View flow
365
+ </a>
366
+ )}
367
+ {hasSchema && (
368
+ <a
369
+ href={buildUrl(`/schemas/${collection}/${resourceId}/${resource?.data?.version}`)}
370
+ class="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
371
+ >
372
+ Schema
373
+ </a>
374
+ )}
375
+ {hasVisualizer && type !== 'flow' && (
376
+ <a
377
+ href={buildUrl(`/visualiser/${collection}/${resourceId}/${resource?.data?.version}`)}
378
+ class="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
379
+ >
380
+ Map
381
+ </a>
382
+ )}
383
+ {repositoryUrl && (
384
+ <a
385
+ href={repositoryUrl}
386
+ target="_blank"
387
+ rel="noopener noreferrer"
388
+ class="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
389
+ >
390
+ Repo
391
+ </a>
392
+ )}
393
+ </span>
394
+ <a href={href} class="text-[rgb(var(--ec-accent))] hover:underline font-medium">
395
+ {type === 'diagram' ? 'View diagram' : 'View docs'}
396
+ </a>
397
+ </span>
398
+ </span>
399
+ </span>
400
+ </>
401
+ )
402
+ }
403
+ </span>
404
+
405
+ <script>
406
+ // Initialize all resource ref tooltips
407
+ function initResourceRefTooltips() {
408
+ const triggers = document.querySelectorAll('.resource-ref-trigger');
409
+
410
+ triggers.forEach((trigger) => {
411
+ const tooltipId = trigger.getAttribute('data-tooltip-id');
412
+ if (!tooltipId) return;
413
+
414
+ const tooltip = document.getElementById(tooltipId);
415
+ if (!tooltip) return;
416
+
417
+ let hideTimeout: ReturnType<typeof setTimeout>;
418
+
419
+ const showTooltip = () => {
420
+ clearTimeout(hideTimeout);
421
+
422
+ // Get trigger position
423
+ const rect = trigger.getBoundingClientRect();
424
+ const tooltipWidth = 320; // w-80 = 20rem = 320px
425
+ const padding = 16;
426
+
427
+ // Calculate left position - try to align with trigger, but keep within viewport
428
+ let left = rect.left;
429
+
430
+ // If tooltip would overflow right edge, align to right edge of viewport
431
+ if (left + tooltipWidth > window.innerWidth - padding) {
432
+ left = window.innerWidth - tooltipWidth - padding;
433
+ }
434
+
435
+ // If tooltip would overflow left edge, align to left edge
436
+ if (left < padding) {
437
+ left = padding;
438
+ }
439
+
440
+ // Position below the trigger
441
+ const top = rect.bottom + 8;
442
+
443
+ tooltip.style.left = `${left}px`;
444
+ tooltip.style.top = `${top}px`;
445
+ tooltip.classList.remove('opacity-0', 'pointer-events-none', 'scale-95');
446
+ tooltip.classList.add('opacity-100', 'pointer-events-auto', 'scale-100');
447
+ };
448
+
449
+ const hideTooltip = () => {
450
+ hideTimeout = setTimeout(() => {
451
+ tooltip.classList.remove('opacity-100', 'pointer-events-auto', 'scale-100');
452
+ tooltip.classList.add('opacity-0', 'pointer-events-none', 'scale-95');
453
+ }, 100);
454
+ };
455
+
456
+ const keepTooltipOpen = () => {
457
+ clearTimeout(hideTimeout);
458
+ };
459
+
460
+ // Trigger events
461
+ trigger.addEventListener('mouseenter', showTooltip);
462
+ trigger.addEventListener('mouseleave', hideTooltip);
463
+ trigger.addEventListener('focus', showTooltip);
464
+ trigger.addEventListener('blur', hideTooltip);
465
+
466
+ // Tooltip events - keep open when hovering tooltip
467
+ tooltip.addEventListener('mouseenter', keepTooltipOpen);
468
+ tooltip.addEventListener('mouseleave', hideTooltip);
469
+ });
470
+ }
471
+
472
+ // Run on initial load
473
+ initResourceRefTooltips();
474
+
475
+ // Re-run after Astro page transitions
476
+ document.addEventListener('astro:page-load', initResourceRefTooltips);
477
+ </script>
@@ -20,6 +20,7 @@ import EntityPropertiesTable from '@components/MDX/EntityPropertiesTable/EntityP
20
20
  import Tabs from '@components/MDX/Tabs/Tabs.astro';
21
21
  import TabItem from '@components/MDX/Tabs/TabItem.astro';
22
22
  import ResourceLink from '@components/MDX/ResourceLink/ResourceLink.astro';
23
+ import ResourceRef from '@components/MDX/ResourceRef/ResourceRef.astro';
23
24
  import Link from '@components/MDX/Link/Link.astro';
24
25
  import Miro from '@components/MDX/Miro/Miro.astro';
25
26
  import Lucid from '@components/MDX/Lucid/Lucid.astro';
@@ -54,6 +55,7 @@ const components = (props: any) => {
54
55
  OpenAPI,
55
56
  ResourceGroupTable: (mdxProp: any) => jsx(ResourceGroupTable, { ...props, ...mdxProp }),
56
57
  ResourceLink: (mdxProp: any) => jsx(ResourceLink, { ...props, ...mdxProp }),
58
+ ResourceRef: (mdxProp: any) => jsx(ResourceRef, { ...props, ...mdxProp }),
57
59
  Schema: (mdxProp: any) => jsx(Schema, { ...props, ...mdxProp }),
58
60
  SchemaViewer: (mdxProp: any) => SchemaViewerPortal({ ...props.data, ...mdxProp }),
59
61
  Step,
@@ -5,21 +5,33 @@
5
5
  * This component loads the sidebar/search data independently of the sidebar UI.
6
6
  * It ensures the search functionality works on all pages, even when the nested
7
7
  * sidebar is not rendered (e.g., /discover pages).
8
+ *
9
+ * The data is fetched from a static JSON file (/api/sidebar-data.json) instead of
10
+ * being embedded inline in every HTML page. This significantly reduces build size
11
+ * for large catalogs.
8
12
  */
9
- import { getNestedSideBarData } from '@stores/sidebar-store/state';
10
-
11
- const props = await getNestedSideBarData();
12
13
  ---
13
14
 
14
- <script is:inline define:vars={{ props }}>
15
- window.sidebarData = props;
16
- </script>
17
-
18
15
  <script>
19
- import { setSidebarData } from '@stores/sidebar-store';
16
+ import { setSidebarData, sidebarStore } from '@stores/sidebar-store';
17
+ import { buildUrl } from '@utils/url-builder';
18
+
19
+ // Only fetch if we haven't already loaded the data
20
+ if (!sidebarStore.get()) {
21
+ const apiUrl = buildUrl('/api/sidebar-data.json', true);
20
22
 
21
- const data = (window as any).sidebarData;
22
- if (data) {
23
- setSidebarData(data);
23
+ fetch(apiUrl)
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
+ });
24
36
  }
25
37
  </script>
@@ -135,6 +135,9 @@ export default function SearchModal() {
135
135
  // Extract all items from nodes
136
136
  const allItems = Object.entries(data.nodes)
137
137
  .map(([key, node]) => {
138
+ // Skip reference entries (string values that point to other keys)
139
+ if (typeof node === 'string') return null;
140
+
138
141
  const url = getUrlForItem(node, key);
139
142
  if (!url) return null;
140
143
 
@@ -232,6 +235,18 @@ export default function SearchModal() {
232
235
  });
233
236
  };
234
237
 
238
+ // Helper to resolve a node, following references if needed
239
+ const resolveNode = (key: string) => {
240
+ if (!data?.nodes) return null;
241
+ const node = data.nodes[key];
242
+ if (!node) return null;
243
+ // If it's a string reference, follow it
244
+ if (typeof node === 'string') {
245
+ return data.nodes[node] as NavNode | undefined;
246
+ }
247
+ return node;
248
+ };
249
+
235
250
  const filteredItems = useMemo(() => {
236
251
  if (query === '') {
237
252
  // Show favorites when search is empty
@@ -239,8 +254,8 @@ export default function SearchModal() {
239
254
  return favorites
240
255
  .slice(0, 5)
241
256
  .map((fav) => {
242
- const node = data?.nodes[fav.nodeKey];
243
- if (!node) return null;
257
+ const node = resolveNode(fav.nodeKey);
258
+ if (!node || typeof node === 'string') return null;
244
259
  const url = getUrlForItem(node, fav.nodeKey);
245
260
  if (!url) return null;
246
261
 
@@ -29,7 +29,7 @@ type SearchResult = {
29
29
  };
30
30
 
31
31
  type Props = {
32
- nodes: Record<string, NavNode>;
32
+ nodes: Record<string, NavNode | string>;
33
33
  onSelectResult: (nodeKey: string, node: NavNode) => void;
34
34
  onSearchChange?: (isSearching: boolean) => void;
35
35
  };
@@ -41,14 +41,20 @@ export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Pro
41
41
 
42
42
  // Pre-process searchable nodes to avoid iterating object on every render
43
43
  // Filter out unversioned keys (e.g., "domain:OrderService") to avoid duplicates with versioned keys (e.g., "domain:OrderService:1.0.0")
44
- const searchableNodes = useMemo(() => {
45
- return Object.entries(nodes).filter(([key, node]) => {
46
- if (node.type === 'group') return false;
44
+ const searchableNodes = useMemo((): Array<[string, NavNode]> => {
45
+ const result: Array<[string, NavNode]> = [];
46
+ for (const [key, node] of Object.entries(nodes)) {
47
+ // Skip string references (unversioned aliases that point to versioned keys)
48
+ if (typeof node === 'string') continue;
49
+ if (node.type === 'group') continue;
47
50
  // Only include versioned keys (those with 3+ parts like "type:id:version")
48
51
  // Unversioned keys (2 parts like "type:id") are aliases to latest version and would cause duplicates
49
52
  const keyParts = key.split(':');
50
- return keyParts.length >= 3;
51
- });
53
+ if (keyParts.length >= 3) {
54
+ result.push([key, node]);
55
+ }
56
+ }
57
+ return result;
52
58
  }, [nodes]);
53
59
 
54
60
  // Get available badges from nodes