@eventcatalog/core 4.0.0-beta.1 → 4.0.0-beta.3

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/bin/eventcatalog.config.d.ts +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-TZTPGQGL.js → chunk-77HKKB65.js} +1 -1
  7. package/dist/{chunk-MBVTS76Q.js → chunk-CMFOHBTI.js} +1 -1
  8. package/dist/{chunk-JTZT2SDF.js → chunk-GHYHLEQZ.js} +1 -1
  9. package/dist/{chunk-U5OTNDVQ.js → chunk-ID4TZKSG.js} +1 -1
  10. package/dist/{chunk-M2ZRT7KQ.js → chunk-J5GFXQT3.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.config.d.cts +40 -7
  15. package/dist/eventcatalog.config.d.ts +40 -7
  16. package/dist/eventcatalog.js +5 -5
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +16 -2
  22. package/eventcatalog/src/components/CopyAsMarkdown.tsx +6 -57
  23. package/eventcatalog/src/content.config.ts +21 -29
  24. package/eventcatalog/src/enterprise/ai/chat-api.ts +4 -2
  25. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +26 -7
  26. package/eventcatalog/src/enterprise/directory/user-team-directory.ts +3 -3
  27. package/eventcatalog/src/enterprise/feature.ts +0 -4
  28. package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +0 -9
  29. package/eventcatalog/src/enterprise/tools/catalog-tools.ts +6 -1
  30. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +343 -282
  31. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +6 -81
  32. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +0 -8
  33. package/eventcatalog/src/stores/sidebar-store/state.ts +4 -4
  34. package/eventcatalog/src/utils/collections/glob-loader.ts +70 -0
  35. package/eventcatalog/src/utils/feature.ts +0 -2
  36. package/package.json +2 -1
  37. package/eventcatalog/src/enterprise/print/_message.data.ts +0 -63
  38. package/eventcatalog/src/enterprise/print/components/PrintHeader.astro +0 -183
  39. package/eventcatalog/src/enterprise/print/components/PrintLayout.astro +0 -271
  40. package/eventcatalog/src/enterprise/print/components/PrintParticipantsTable.astro +0 -64
  41. package/eventcatalog/src/enterprise/print/components/PrintSchemaPropertiesTable.astro +0 -65
  42. package/eventcatalog/src/enterprise/print/components/PrintSchemaViewer.tsx +0 -55
  43. package/eventcatalog/src/enterprise/print/components/PrintSection.astro +0 -17
  44. package/eventcatalog/src/enterprise/print/message.astro +0 -353
  45. package/eventcatalog/src/enterprise/print/utils.ts +0 -58
  46. package/eventcatalog/src/utils/sidebar-visibility.ts +0 -41
@@ -10,18 +10,7 @@ interface Props {
10
10
  reducedContentPadding?: boolean;
11
11
  }
12
12
 
13
- import {
14
- TableProperties,
15
- House,
16
- FileText,
17
- SquareDashedMousePointerIcon,
18
- FileCode,
19
- Waypoints,
20
- PanelLeftClose,
21
- UserRound,
22
- UsersRound,
23
- Settings,
24
- } from 'lucide-react';
13
+ import * as LucideIcons from 'lucide-react';
25
14
  import BaseLayout from './BaseLayout.astro';
26
15
  import Header from '../components/Header.astro';
27
16
  import SideNav from '../components/SideNav/SideNav.astro';
@@ -59,10 +48,9 @@ import { getDataProducts } from '@utils/collections/data-products';
59
48
  import { isCollectionVisibleInCatalog } from '@eventcatalog';
60
49
  import { buildUrl } from '@utils/url-builder';
61
50
  import { getQueries } from '@utils/collections/queries';
62
- import { filterSidebarItems } from '@utils/sidebar-visibility';
63
51
  import { getFirstNavigationPage } from '@enterprise/custom-documentation/utils/custom-docs';
64
52
 
65
- import { isEmbedEnabled, isCustomStylesEnabled, isEventCatalogScaleEnabled, isCustomDocsEnabled, isSSR } from '@utils/feature';
53
+ import { isEmbedEnabled, isCustomStylesEnabled, isCustomDocsEnabled, isSSR } from '@utils/feature';
66
54
 
67
55
  const customDocs = await getCollection('customPages');
68
56
  const firstCustomDocsPage = isCustomDocsEnabled() && customDocs.length > 0 ? await getFirstNavigationPage() : null;
@@ -132,163 +120,304 @@ const getDefaultUrl = (route: string, defaultValue: string) => {
132
120
  return buildUrl(defaultValue);
133
121
  };
134
122
 
135
- const userSideBarConfiguration = config.sidebar || [];
136
-
137
- const navigationItems = filterSidebarItems(
138
- [
139
- {
140
- id: '/',
141
- label: 'Home',
142
- icon: House,
143
- href: buildUrl(config.landingPage || '/'),
144
- current:
145
- currentPath === '/' ||
146
- (currentPath.includes('/docs') && !currentPath.includes('/docs/custom')) ||
147
- currentPath.includes('/architecture/') ||
148
- currentPath.includes('/visualiser') ||
149
- (currentPath.includes('/schemas') &&
150
- !currentPath.includes('/schemas/explorer') &&
151
- !currentPath.includes('/schemas/fields')),
152
- },
153
- {
154
- id: '/docs/custom',
155
- aliases: ['/docs'],
156
- label: 'Documentation',
157
- icon: FileText,
158
- href: getDefaultUrl('docs/custom', '/docs/custom'),
159
- current: currentPath.includes('/docs/custom'),
160
- isPremium: true,
161
- visible: isCustomDocsEnabled() && customDocs.length > 0,
162
- },
163
- ],
164
- userSideBarConfiguration
165
- );
166
-
167
- const studioNavigationItem = filterSidebarItems(
168
- [
169
- {
170
- id: '/studio',
171
- label: 'EventCatalog Studio',
172
- icon: SquareDashedMousePointerIcon,
173
- href: buildUrl('/studio'),
174
- current: currentPath.includes('/studio'),
175
- },
176
- ],
177
- userSideBarConfiguration
178
- );
179
-
180
- const premiumFeatures: Array<{
181
- id: string;
182
- label: string;
183
- icon: any;
184
- href: string;
185
- current: boolean;
186
- isPremium?: boolean;
187
- }> = [];
188
-
189
123
  const internalServices = services.filter((service) => !service.data.externalSystem);
190
124
  const externalSystems = services.filter((service) => service.data.externalSystem);
191
125
  const discoverTargets = [
192
- { id: '/discover/domains', aliases: ['/discover'], href: buildUrl('/discover/domains'), count: domains.length },
193
- { id: '/discover/systems', aliases: ['/discover'], href: buildUrl('/discover/systems'), count: systems.length },
126
+ { id: '/discover/domains', href: buildUrl('/discover/domains'), count: domains.length },
127
+ { id: '/discover/systems', href: buildUrl('/discover/systems'), count: systems.length },
194
128
  {
195
129
  id: '/discover/services',
196
- aliases: ['/discover'],
197
130
  href: buildUrl('/discover/services'),
198
131
  count: internalServices.length,
199
132
  },
200
133
  {
201
134
  id: '/discover/external-systems',
202
- aliases: ['/discover'],
203
135
  href: buildUrl('/discover/external-systems'),
204
136
  count: externalSystems.length,
205
137
  },
206
- { id: '/discover/agents', aliases: ['/discover'], href: buildUrl('/discover/agents'), count: agents.length },
207
- { id: '/discover/adrs', aliases: ['/discover'], href: buildUrl('/discover/adrs'), count: adrs.length },
138
+ { id: '/discover/agents', href: buildUrl('/discover/agents'), count: agents.length },
139
+ { id: '/discover/adrs', href: buildUrl('/discover/adrs'), count: adrs.length },
208
140
  {
209
141
  id: '/discover/data-products',
210
- aliases: ['/discover'],
211
142
  href: buildUrl('/discover/data-products'),
212
143
  count: dataProducts.length,
213
144
  },
214
- { id: '/discover/containers', aliases: ['/discover'], href: buildUrl('/discover/containers'), count: containers.length },
215
- { id: '/discover/events', aliases: ['/discover'], href: buildUrl('/discover/events'), count: events.length },
216
- { id: '/discover/commands', aliases: ['/discover'], href: buildUrl('/discover/commands'), count: commands.length },
217
- { id: '/discover/queries', aliases: ['/discover'], href: buildUrl('/discover/queries'), count: queries.length },
218
- { id: '/discover/flows', aliases: ['/discover'], href: buildUrl('/discover/flows'), count: flows.length },
145
+ { id: '/discover/containers', href: buildUrl('/discover/containers'), count: containers.length },
146
+ { id: '/discover/events', href: buildUrl('/discover/events'), count: events.length },
147
+ { id: '/discover/commands', href: buildUrl('/discover/commands'), count: commands.length },
148
+ { id: '/discover/queries', href: buildUrl('/discover/queries'), count: queries.length },
149
+ { id: '/discover/flows', href: buildUrl('/discover/flows'), count: flows.length },
219
150
  ];
220
- const defaultDiscoverTarget = filterSidebarItems(discoverTargets, userSideBarConfiguration).find((target) => target.count > 0);
151
+ const defaultDiscoverTarget = discoverTargets.find((target) => target.count > 0);
221
152
 
222
- const browseItems = filterSidebarItems(
223
- [
224
- {
225
- id: '/discover',
226
- aliases: ['/discover'],
227
- label: 'Resources',
228
- icon: TableProperties,
229
- href: defaultDiscoverTarget?.href ?? buildUrl('/discover/domains'),
230
- current: currentPath === buildUrl('/discover') || currentPath.startsWith(buildUrl('/discover/', true)),
231
- visible: Boolean(defaultDiscoverTarget),
232
- },
233
- {
234
- id: '/schemas/explorer',
235
- label: 'Schemas & APIs',
236
- icon: FileCode,
237
- href: buildUrl('/schemas/explorer'),
238
- current: currentPath.includes('/schemas/explorer') && !currentPath.includes('/schemas/fields'),
239
- },
240
- ...(isSSR()
241
- ? [
242
- {
243
- id: '/schemas/fields',
244
- label: 'Schema Insights',
245
- icon: Waypoints,
246
- href: buildUrl('/schemas/fields'),
247
- current: currentPath.includes('/schemas/fields'),
248
- },
249
- ]
250
- : []),
251
- ],
252
- userSideBarConfiguration
253
- ).filter((item) => item.visible !== false);
254
-
255
- const organizationItems = filterSidebarItems(
256
- [
257
- {
258
- id: '/directory/teams',
259
- aliases: ['/directory'],
260
- label: 'Teams',
261
- icon: UsersRound,
262
- href: buildUrl('/directory/teams'),
263
- current: currentPath === buildUrl('/directory/teams'),
264
- },
265
- {
266
- id: '/directory/users',
267
- aliases: ['/directory'],
268
- label: 'Users',
269
- icon: UserRound,
270
- href: buildUrl('/directory/users'),
271
- current: currentPath === buildUrl('/directory/users'),
272
- },
273
- ],
274
- userSideBarConfiguration
275
- );
153
+ type NavigationItemConfig = {
154
+ id: string;
155
+ label?: string;
156
+ icon?: string;
157
+ href?: string;
158
+ visible?: boolean;
159
+ match?: string | string[];
160
+ };
276
161
 
277
- const settingsItems = filterSidebarItems(
278
- [
279
- {
280
- id: '/settings/general',
281
- aliases: ['/settings'],
282
- label: 'Settings',
283
- icon: Settings,
284
- href: buildUrl('/settings/general'),
285
- current: currentPath.startsWith(buildUrl('/settings')),
286
- },
287
- ],
288
- userSideBarConfiguration
289
- );
162
+ type NavigationGroupConfig = {
163
+ id: string;
164
+ label?: string;
165
+ visible?: boolean;
166
+ position?: 'top' | 'bottom';
167
+ items?: NavigationItemConfig[];
168
+ };
169
+
170
+ type VerticalNavigationItem = Required<Pick<NavigationItemConfig, 'id' | 'label' | 'href'>> & {
171
+ icon: string;
172
+ current: boolean;
173
+ matchId: string;
174
+ match?: string | string[];
175
+ target?: string;
176
+ rel?: string;
177
+ Icon: any;
178
+ };
179
+
180
+ const toPascalCase = (value: string) =>
181
+ value
182
+ .split(/[-_\s]+/)
183
+ .filter(Boolean)
184
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
185
+ .join('');
186
+
187
+ const resolveLucideIcon = (icon?: string) => {
188
+ if (!icon) return LucideIcons.Circle;
189
+
190
+ const icons = LucideIcons as unknown as Record<string, any>;
191
+ return (
192
+ icons[icon] || icons[`${icon}Icon`] || icons[toPascalCase(icon)] || icons[`${toPascalCase(icon)}Icon`] || LucideIcons.Circle
193
+ );
194
+ };
195
+
196
+ const normalizeNavigationHref = (href: string) => {
197
+ if (/^(https?:)?\/\//.test(href) || href.startsWith('mailto:') || href.startsWith('#')) {
198
+ return href;
199
+ }
200
+
201
+ return buildUrl(href);
202
+ };
203
+
204
+ const isExternalNavigationHref = (href: string) => /^https?:\/\//.test(href);
205
+
206
+ const normalizeNavigationMatch = (match?: string | string[]) => {
207
+ if (!match) return undefined;
208
+
209
+ const normalizeMatch = (value: string) => (isExternalNavigationHref(value) ? value : buildUrl(value, true));
210
+ return Array.isArray(match) ? match.map(normalizeMatch) : normalizeMatch(match);
211
+ };
212
+
213
+ const navigationBuiltinAliases: Record<string, string> = {
214
+ home: '/',
215
+ docs: '/docs/custom',
216
+ catalog: '/discover',
217
+ schemas: '/schemas/explorer',
218
+ 'schema-insights': '/schemas/fields',
219
+ teams: '/directory/teams',
220
+ users: '/directory/users',
221
+ settings: '/settings/general',
222
+ studio: '/studio',
223
+ };
224
+
225
+ const builtinNavigationItems: Record<string, Omit<VerticalNavigationItem, 'current' | 'Icon'>> = {
226
+ '/': {
227
+ id: '/',
228
+ matchId: '/',
229
+ label: 'Home',
230
+ icon: 'House',
231
+ href: buildUrl(config.landingPage || '/'),
232
+ },
233
+ '/docs/custom': {
234
+ id: '/docs/custom',
235
+ matchId: '/docs/custom',
236
+ label: 'Docs',
237
+ icon: 'FileText',
238
+ href: getDefaultUrl('docs/custom', '/docs/custom'),
239
+ },
240
+ '/studio': {
241
+ id: '/studio',
242
+ matchId: '/studio',
243
+ label: 'EventCatalog Studio',
244
+ icon: 'SquareDashedMousePointer',
245
+ href: buildUrl('/studio'),
246
+ },
247
+ '/discover': {
248
+ id: '/discover',
249
+ matchId: '/discover',
250
+ label: 'Catalog',
251
+ icon: 'TableProperties',
252
+ href: defaultDiscoverTarget?.href ?? buildUrl('/discover/domains'),
253
+ },
254
+ '/schemas/explorer': {
255
+ id: '/schemas/explorer',
256
+ matchId: '/schemas/explorer',
257
+ label: 'Schemas & APIs',
258
+ icon: 'FileCode',
259
+ href: buildUrl('/schemas/explorer'),
260
+ },
261
+ '/schemas/fields': {
262
+ id: '/schemas/fields',
263
+ matchId: '/schemas/fields',
264
+ label: 'Schema Insights',
265
+ icon: 'Waypoints',
266
+ href: buildUrl('/schemas/fields'),
267
+ },
268
+ '/directory/teams': {
269
+ id: '/directory/teams',
270
+ matchId: '/directory/teams',
271
+ label: 'Teams',
272
+ icon: 'UsersRound',
273
+ href: buildUrl('/directory/teams'),
274
+ },
275
+ '/directory/users': {
276
+ id: '/directory/users',
277
+ matchId: '/directory/users',
278
+ label: 'Users',
279
+ icon: 'UserRound',
280
+ href: buildUrl('/directory/users'),
281
+ },
282
+ '/settings/general': {
283
+ id: '/settings/general',
284
+ matchId: '/settings/general',
285
+ label: 'Settings',
286
+ icon: 'Settings',
287
+ href: buildUrl('/settings/general'),
288
+ },
289
+ };
290
+
291
+ const defaultNavigationGroups: NavigationGroupConfig[] = [
292
+ {
293
+ id: 'main',
294
+ items: [{ id: '/' }, { id: '/docs/custom', visible: isCustomDocsEnabled() && customDocs.length > 0 }],
295
+ },
296
+ {
297
+ id: 'browse',
298
+ label: 'Browse',
299
+ items: [
300
+ { id: '/discover', visible: Boolean(defaultDiscoverTarget) },
301
+ { id: '/schemas/explorer' },
302
+ { id: '/schemas/fields', visible: isSSR() },
303
+ ],
304
+ },
305
+ {
306
+ id: 'organization',
307
+ label: 'Organization',
308
+ items: [{ id: '/directory/teams' }, { id: '/directory/users' }],
309
+ },
310
+ {
311
+ id: 'settings',
312
+ position: 'bottom',
313
+ items: [{ id: '/settings/general' }],
314
+ },
315
+ ];
316
+
317
+ const isNavigationItemCurrent = (item: Pick<VerticalNavigationItem, 'href' | 'matchId' | 'match'>, pathname: string) => {
318
+ const matchId = item.matchId;
319
+
320
+ if (matchId === '/') {
321
+ const itemPath = new URL(item.href, Astro.url.origin).pathname;
322
+ return (
323
+ pathname === itemPath ||
324
+ (pathname.includes('/docs') && !pathname.includes('/docs/custom')) ||
325
+ pathname.includes('/architecture/') ||
326
+ pathname.includes('/visualiser') ||
327
+ (pathname.includes('/schemas') && !pathname.includes('/schemas/explorer') && !pathname.includes('/schemas/fields'))
328
+ );
329
+ }
330
+
331
+ if (matchId === '/schemas/explorer') {
332
+ return pathname.includes('/schemas/explorer') && !pathname.includes('/schemas/fields');
333
+ }
334
+
335
+ if (matchId === '/schemas/fields') {
336
+ return pathname.includes('/schemas/fields');
337
+ }
338
+
339
+ if (matchId === '/discover') {
340
+ return pathname === buildUrl('/discover', true) || pathname.endsWith('/discover') || pathname.includes('/discover/');
341
+ }
342
+
343
+ if (matchId === '/studio') {
344
+ return pathname.includes('/studio');
345
+ }
346
+
347
+ if (matchId === '/docs/custom') {
348
+ return pathname.includes('/docs/custom');
349
+ }
350
+
351
+ if (matchId === '/settings/general') {
352
+ return pathname.startsWith(buildUrl('/settings', true));
353
+ }
290
354
 
291
- const currentNavigationItem = [...navigationItems, ...studioNavigationItem, ...premiumFeatures].find((item) => item.current);
355
+ const configuredMatches = Array.isArray(item.match) ? item.match : item.match ? [item.match] : [];
356
+ if (configuredMatches.length > 0) {
357
+ return configuredMatches.some((match) => {
358
+ if (isExternalNavigationHref(match)) return false;
359
+
360
+ return pathname === buildUrl(match, true) || pathname.startsWith(buildUrl(match, true));
361
+ });
362
+ }
363
+
364
+ if (isExternalNavigationHref(item.href)) return false;
365
+
366
+ const itemPath = new URL(item.href, Astro.url.origin).pathname;
367
+ return pathname === itemPath;
368
+ };
369
+
370
+ const resolveNavigationItem = (item: NavigationItemConfig): VerticalNavigationItem | null => {
371
+ if (item.visible === false) return null;
372
+
373
+ const builtinId = navigationBuiltinAliases[item.id] || item.id;
374
+ const builtin = builtinNavigationItems[builtinId];
375
+ const href = item.href ? normalizeNavigationHref(item.href) : builtin?.href;
376
+
377
+ if (!href) return null;
378
+
379
+ const resolvedItem = {
380
+ id: item.id,
381
+ matchId: builtin?.matchId || item.id,
382
+ label: item.label || builtin?.label || item.id,
383
+ icon: item.icon || builtin?.icon || 'Circle',
384
+ href,
385
+ match: item.match,
386
+ };
387
+
388
+ return {
389
+ ...resolvedItem,
390
+ current: isNavigationItemCurrent(resolvedItem, currentPath),
391
+ target: isExternalNavigationHref(href) ? '_blank' : undefined,
392
+ rel: isExternalNavigationHref(href) ? 'noopener noreferrer' : undefined,
393
+ Icon: resolveLucideIcon(resolvedItem.icon),
394
+ };
395
+ };
396
+
397
+ const resolveNavigationGroups = (groups: NavigationGroupConfig[]) =>
398
+ groups
399
+ .filter((group) => group.visible !== false)
400
+ .map((group) => ({
401
+ id: group.id,
402
+ label: group.label,
403
+ position: group.position || 'top',
404
+ items: (group.items || []).map(resolveNavigationItem).filter((item): item is VerticalNavigationItem => Boolean(item)),
405
+ }))
406
+ .filter((group) => group.items.length > 0);
407
+
408
+ const navigationGroups = resolveNavigationGroups(
409
+ Array.isArray(config.navigation?.groups) ? (config.navigation.groups as NavigationGroupConfig[]) : defaultNavigationGroups
410
+ );
411
+ const topNavigationGroups = navigationGroups.filter((group) => group.position !== 'bottom');
412
+ const bottomNavigationGroups = navigationGroups.filter((group) => group.position === 'bottom');
413
+ const clientNavigationItems = navigationGroups.flatMap((group) =>
414
+ group.items.map(({ id, href, matchId, match }) => ({
415
+ id,
416
+ href,
417
+ matchId,
418
+ match: normalizeNavigationMatch(match),
419
+ }))
420
+ );
292
421
  const { title, description, showNestedSideBar = true, showHeader = true, reducedContentPadding = false } = Astro.props;
293
422
 
294
423
  const canPageBeEmbedded = isEmbedEnabled();
@@ -537,133 +666,72 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
537
666
 
538
667
  <nav class="flex flex-col flex-1 overflow-y-auto px-3 py-4 gap-1">
539
668
  {
540
- navigationItems.map((item) => {
541
- return (
542
- <a
543
- id={item.id}
544
- data-role="nav-item"
545
- href={item.href}
546
- aria-label={item.label}
547
- class={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
548
- item.current
549
- ? 'border-[rgb(var(--ec-accent)/0.2)] bg-[rgb(var(--ec-page-bg)/0.88)] text-[rgb(var(--ec-accent))] shadow-sm'
550
- : 'border-transparent text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]'
551
- }`}
552
- title={item.label}
553
- >
554
- <item.icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
555
- <span class="nav-item-label">{item.label}</span>
556
- </a>
557
- );
558
- })
559
- }
560
-
561
- {
562
- browseItems.length > 0 && (
563
- <Fragment>
564
- <hr class="rail-divider my-3 border-t border-[rgb(var(--ec-sidebar-border)/0.7)]" />
565
-
566
- <div class="rail-section-label px-3 pb-1 text-[0.65rem] font-semibold tracking-[0.18em] uppercase text-[rgb(var(--ec-sidebar-text)/0.5)]">
567
- Browse
568
- </div>
569
-
570
- {browseItems.map((item) => (
571
- <a
572
- id={item.id}
573
- href={item.href}
574
- data-role="secondary-nav-item"
575
- aria-label={item.label}
576
- class={`nav-secondary-item flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
577
- item.current
578
- ? 'border-[rgb(var(--ec-accent)/0.2)] bg-[rgb(var(--ec-page-bg)/0.88)] text-[rgb(var(--ec-accent))] shadow-sm'
579
- : 'border-transparent text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]'
580
- }`}
581
- title={item.label}
582
- >
583
- <item.icon className="h-3.5 w-3.5 flex-shrink-0" aria-hidden="true" />
584
- <span class="nav-item-label">{item.label}</span>
585
- </a>
586
- ))}
587
- </Fragment>
588
- )
589
- }
590
-
591
- {
592
- organizationItems.length > 0 && (
669
+ topNavigationGroups.map((group, groupIndex) => (
593
670
  <Fragment>
594
- <hr class="rail-divider my-3 border-t border-[rgb(var(--ec-sidebar-border)/0.7)]" />
671
+ {groupIndex > 0 && <hr class="rail-divider my-3 border-t border-[rgb(var(--ec-sidebar-border)/0.7)]" />}
595
672
 
596
- <div class="rail-section-label px-3 pb-1 text-[0.65rem] font-semibold tracking-[0.18em] uppercase text-[rgb(var(--ec-sidebar-text)/0.5)]">
597
- Organization
598
- </div>
673
+ {group.label && (
674
+ <div class="rail-section-label px-3 pb-1 text-[0.65rem] font-semibold tracking-[0.18em] uppercase text-[rgb(var(--ec-sidebar-text)/0.5)]">
675
+ {group.label}
676
+ </div>
677
+ )}
599
678
 
600
- {organizationItems.map((item) => (
679
+ {group.items.map((item) => (
601
680
  <a
602
681
  id={item.id}
603
682
  href={item.href}
604
- data-role="secondary-nav-item"
683
+ target={item.target}
684
+ rel={item.rel}
685
+ data-role="vertical-nav-item"
605
686
  aria-label={item.label}
606
- class={`nav-secondary-item flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
687
+ class={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
607
688
  item.current
608
689
  ? 'border-[rgb(var(--ec-accent)/0.2)] bg-[rgb(var(--ec-page-bg)/0.88)] text-[rgb(var(--ec-accent))] shadow-sm'
609
690
  : 'border-transparent text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]'
610
691
  }`}
611
692
  title={item.label}
612
693
  >
613
- <item.icon className="h-3.5 w-3.5 flex-shrink-0" aria-hidden="true" />
694
+ <item.Icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
614
695
  <span class="nav-item-label">{item.label}</span>
615
696
  </a>
616
697
  ))}
617
698
  </Fragment>
618
- )
619
- }
620
-
621
- {premiumFeatures.length > 0 && <hr class="rail-divider my-2 border-t border-[rgb(var(--ec-sidebar-border)/0.7)]" />}
622
-
623
- {
624
- premiumFeatures.map((item) => (
625
- <a
626
- id={item.id}
627
- data-role="nav-item"
628
- href={item.href}
629
- aria-label={item.label}
630
- class={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
631
- item.current
632
- ? 'border-[rgb(var(--ec-accent)/0.2)] bg-[rgb(var(--ec-page-bg)/0.88)] text-[rgb(var(--ec-accent))] shadow-sm'
633
- : 'border-transparent text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]'
634
- }`}
635
- title={item.label}
636
- >
637
- <item.icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
638
- <span class="nav-item-label">{item.label}</span>
639
- </a>
640
699
  ))
641
700
  }
642
701
 
643
702
  <div class="mt-auto pt-3">
644
703
  {
645
- settingsItems.length > 0 && (
704
+ bottomNavigationGroups.map((group) => (
646
705
  <Fragment>
647
706
  <hr class="rail-divider mb-3 border-t border-[rgb(var(--ec-sidebar-border)/0.7)]" />
648
- {settingsItems.map((item) => (
707
+
708
+ {group.label && (
709
+ <div class="rail-section-label px-3 pb-1 text-[0.65rem] font-semibold tracking-[0.18em] uppercase text-[rgb(var(--ec-sidebar-text)/0.5)]">
710
+ {group.label}
711
+ </div>
712
+ )}
713
+
714
+ {group.items.map((item) => (
649
715
  <a
650
716
  id={item.id}
651
717
  href={item.href}
652
- data-role="secondary-nav-item"
718
+ target={item.target}
719
+ rel={item.rel}
720
+ data-role="vertical-nav-item"
653
721
  aria-label={item.label}
654
- class={`nav-secondary-item flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
722
+ class={`flex items-center gap-3 px-3 py-2.5 rounded-xl border text-[13px] font-medium transition-all duration-150 ${
655
723
  item.current
656
724
  ? 'border-[rgb(var(--ec-accent)/0.2)] bg-[rgb(var(--ec-page-bg)/0.88)] text-[rgb(var(--ec-accent))] shadow-sm'
657
725
  : 'border-transparent text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]'
658
726
  }`}
659
727
  title={item.label}
660
728
  >
661
- <item.icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
729
+ <item.Icon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
662
730
  <span class="nav-item-label">{item.label}</span>
663
731
  </a>
664
732
  ))}
665
733
  </Fragment>
666
- )
734
+ ))
667
735
  }
668
736
  </div>
669
737
  </nav>
@@ -675,7 +743,7 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
675
743
  aria-label="Collapse sidebar"
676
744
  class="flex items-center gap-3 px-3 py-2.5 rounded-xl border border-transparent text-[13px] font-medium text-[rgb(var(--ec-sidebar-text))] hover:border-[rgb(var(--ec-sidebar-border)/0.65)] hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))] transition-all duration-150 w-full"
677
745
  >
678
- <PanelLeftClose className="h-4 w-4 flex-shrink-0 nav-collapse-icon" aria-hidden="true" />
746
+ <LucideIcons.PanelLeftClose className="h-4 w-4 flex-shrink-0 nav-collapse-icon" aria-hidden="true" />
679
747
  <span class="nav-label">Collapse</span>
680
748
  </button>
681
749
  </div>
@@ -725,8 +793,7 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
725
793
  <ClientRouter />
726
794
  <script
727
795
  define:vars={{
728
- navigationItems,
729
- currentNavigationItem,
796
+ clientNavigationItems,
730
797
  canPageBeEmbedded,
731
798
  verticalNavAutoCollapsePaths,
732
799
  }}
@@ -746,10 +813,13 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
746
813
  'hover:text-[rgb(var(--ec-page-text))]',
747
814
  ];
748
815
 
749
- const isNavItemCurrent = (id, pathname) => {
750
- if (id === '/') {
816
+ const isNavItemCurrent = (item, pathname) => {
817
+ const matchId = item?.matchId || item?.id;
818
+
819
+ if (matchId === '/') {
820
+ const targetPath = item?.href ? new URL(item.href, window.location.origin).pathname : '/';
751
821
  return (
752
- pathname === '/' ||
822
+ pathname === targetPath ||
753
823
  (pathname.includes('/docs') && !pathname.includes('/docs/custom')) ||
754
824
  pathname.includes('/architecture/') ||
755
825
  pathname.includes('/visualiser') ||
@@ -757,52 +827,57 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
757
827
  );
758
828
  }
759
829
 
760
- if (id === '/schemas/explorer') {
830
+ if (matchId === '/schemas/explorer') {
761
831
  return pathname.includes('/schemas/explorer') && !pathname.includes('/schemas/fields');
762
832
  }
763
833
 
764
- if (id === '/schemas/fields') {
834
+ if (matchId === '/schemas/fields') {
765
835
  return pathname.includes('/schemas/fields');
766
836
  }
767
837
 
768
- if (id === '/discover') {
769
- return pathname === '/discover' || pathname.includes('/discover/');
838
+ if (matchId === '/discover') {
839
+ return pathname.endsWith('/discover') || pathname.includes('/discover/');
770
840
  }
771
841
 
772
- if (id === '/studio') {
842
+ if (matchId === '/studio') {
773
843
  return pathname.includes('/studio');
774
844
  }
775
845
 
776
- if (id === '/docs/custom') {
846
+ if (matchId === '/docs/custom') {
777
847
  return pathname.includes('/docs/custom');
778
848
  }
779
849
 
850
+ if (matchId === '/settings/general') {
851
+ return pathname.includes('/settings');
852
+ }
853
+
854
+ const matches = Array.isArray(item?.match) ? item.match : item?.match ? [item.match] : [];
855
+ if (matches.length > 0) {
856
+ return matches.some((match) => {
857
+ if (/^https?:\/\//.test(match)) return false;
858
+
859
+ return pathname === match || pathname.startsWith(match);
860
+ });
861
+ }
862
+
863
+ if (item?.href) {
864
+ if (/^https?:\/\//.test(item.href)) return false;
865
+
866
+ const targetPath = new URL(item.href, window.location.origin).pathname;
867
+ return pathname === targetPath;
868
+ }
869
+
780
870
  return false;
781
871
  };
782
872
 
783
873
  const syncNavItemStates = () => {
784
874
  const pathname = window.location.pathname;
785
- const navItems = document.querySelectorAll('[data-role="nav-item"]');
786
- const secondaryNavItems = document.querySelectorAll('[data-role="secondary-nav-item"]');
875
+ const navItems = document.querySelectorAll('[data-role="vertical-nav-item"]');
787
876
 
788
877
  navItems.forEach((item) => {
789
878
  const id = item.getAttribute('id');
790
- const isCurrent = id ? isNavItemCurrent(id, pathname) : false;
791
-
792
- if (isCurrent) {
793
- item.classList.remove(...INACTIVE_NAV_CLASSES);
794
- item.classList.add(...ACTIVE_NAV_CLASSES);
795
- } else {
796
- item.classList.remove(...ACTIVE_NAV_CLASSES);
797
- item.classList.add(...INACTIVE_NAV_CLASSES);
798
- }
799
- });
800
-
801
- secondaryNavItems.forEach((item) => {
802
- const id = item.getAttribute('id');
803
- const href = item.getAttribute('href');
804
- const targetPath = href ? new URL(href, window.location.origin).pathname : null;
805
- const isCurrent = id === '/discover' ? isNavItemCurrent(id, pathname) : targetPath ? pathname === targetPath : false;
879
+ const navigationItem = clientNavigationItems.find((navItem) => navItem.id === id);
880
+ const isCurrent = navigationItem ? isNavItemCurrent(navigationItem, pathname) : false;
806
881
 
807
882
  if (isCurrent) {
808
883
  item.classList.remove(...INACTIVE_NAV_CLASSES);
@@ -939,20 +1014,6 @@ const verticalNavAutoCollapsePaths = [buildUrl('/discover', true), buildUrl('/do
939
1014
 
940
1015
  syncNavItemStates();
941
1016
 
942
- const navItems = document.querySelectorAll('[data-role="nav-item"]');
943
-
944
- // Navigation items simply navigate to their href - no toggle logic
945
- navItems.forEach((item) => {
946
- item.onclick = (e) => {
947
- const id = item.getAttribute('id');
948
- const navItem = navigationItems.find((navItem) => navItem.id === id);
949
-
950
- if (navItem && navItem.href) {
951
- window.location.href = navItem.href;
952
- }
953
- };
954
- });
955
-
956
1017
  if (verticalNavToggle) {
957
1018
  verticalNavToggle.onclick = () => {
958
1019
  const isCollapsed = verticalNavRail?.getAttribute('data-collapsed') === 'true';