@eventcatalog/core 3.48.4 → 4.0.0-beta.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 (109) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/catalog-to-astro-content-directory.cjs +1 -1
  6. package/dist/chunk-BWN544LB.js +73 -0
  7. package/dist/{chunk-XNVSSL4W.js → chunk-JTZT2SDF.js} +1 -1
  8. package/dist/{chunk-JY4H7A4S.js → chunk-M2ZRT7KQ.js} +1 -1
  9. package/dist/{chunk-SY4XL6OQ.js → chunk-MBVTS76Q.js} +1 -1
  10. package/dist/{chunk-KDLSBQRP.js → chunk-TZTPGQGL.js} +1 -1
  11. package/dist/{chunk-FNDSHOV2.js → chunk-U5OTNDVQ.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/core-node-modules.cjs +108 -0
  15. package/dist/core-node-modules.d.cts +7 -0
  16. package/dist/core-node-modules.d.ts +7 -0
  17. package/dist/core-node-modules.js +8 -0
  18. package/dist/docs/development/components/components/15-prompt.md +5 -1
  19. package/dist/eventcatalog.cjs +124 -48
  20. package/dist/eventcatalog.js +26 -15
  21. package/dist/generate.cjs +1 -1
  22. package/dist/generate.js +3 -3
  23. package/dist/utils/cli-logger.cjs +1 -1
  24. package/dist/utils/cli-logger.js +2 -2
  25. package/eventcatalog/astro.config.mjs +27 -23
  26. package/eventcatalog/public/icons/languages/java.svg +1 -0
  27. package/eventcatalog/public/icons/payments/stripe.svg +1 -0
  28. package/eventcatalog/src/components/Badge.astro +10 -10
  29. package/eventcatalog/src/components/CopyAsMarkdown.tsx +154 -0
  30. package/eventcatalog/src/components/Grids/DomainGrid.tsx +207 -44
  31. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -20
  32. package/eventcatalog/src/components/Grids/SystemGrid.tsx +185 -0
  33. package/eventcatalog/src/components/MDX/ADRTable/ADRTable.astro +99 -0
  34. package/eventcatalog/src/components/MDX/ADRTable/ADRTable.client.tsx +400 -0
  35. package/eventcatalog/src/components/MDX/ContextDiagram/ContextDiagramPortal.tsx +21 -0
  36. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +1 -0
  37. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +42 -6
  38. package/eventcatalog/src/components/MDX/Prompt/Prompt.astro +62 -9
  39. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +8 -1
  40. package/eventcatalog/src/components/MDX/SystemContextMap/SystemContextMapPortal.tsx +22 -0
  41. package/eventcatalog/src/components/MDX/components.tsx +7 -0
  42. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +1 -1
  43. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +67 -37
  44. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +47 -0
  45. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +10 -2
  46. package/eventcatalog/src/components/Tables/Discover/columns.tsx +115 -27
  47. package/eventcatalog/src/components/Tables/Discover/index.ts +7 -1
  48. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +267 -0
  49. package/eventcatalog/src/content.config.ts +96 -8
  50. package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +4 -1
  51. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +12 -13
  52. package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +6 -0
  53. package/eventcatalog/src/enterprise/feature.ts +3 -1
  54. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +96 -140
  55. package/eventcatalog/src/layouts/VisualiserLayout.astro +66 -0
  56. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +4 -2
  57. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +2 -0
  58. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +15 -15
  59. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +1 -0
  60. package/eventcatalog/src/pages/discover/[type]/index.astro +47 -2
  61. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +15 -14
  62. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +13 -14
  63. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +11 -1
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +81 -26
  66. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +89 -0
  67. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +85 -0
  68. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +2 -0
  69. package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +2 -0
  70. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +1 -0
  71. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +24 -1
  72. package/eventcatalog/src/pages/docs/users/[id]/index.astro +24 -1
  73. package/eventcatalog/src/pages/index.astro +30 -7
  74. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +1 -0
  75. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -1
  76. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +3 -0
  77. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +94 -0
  78. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +51 -0
  79. package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +41 -0
  80. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +91 -0
  81. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +51 -0
  82. package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +3 -1
  83. package/eventcatalog/src/stores/sidebar-store/builders/agent.ts +3 -1
  84. package/eventcatalog/src/stores/sidebar-store/builders/container.ts +1 -1
  85. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +1 -1
  86. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +127 -49
  87. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +1 -1
  88. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +4 -2
  89. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +2 -1
  90. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +166 -0
  91. package/eventcatalog/src/stores/sidebar-store/state.ts +99 -6
  92. package/eventcatalog/src/types/index.ts +2 -0
  93. package/eventcatalog/src/utils/collection-colors.ts +6 -1
  94. package/eventcatalog/src/utils/collections/domains.ts +35 -0
  95. package/eventcatalog/src/utils/collections/icons.ts +4 -5
  96. package/eventcatalog/src/utils/collections/systems.ts +110 -0
  97. package/eventcatalog/src/utils/collections/teams.ts +15 -10
  98. package/eventcatalog/src/utils/collections/users.ts +7 -4
  99. package/eventcatalog/src/utils/collections/util.ts +2 -0
  100. package/eventcatalog/src/utils/icon-map.ts +20 -0
  101. package/eventcatalog/src/utils/icon.ts +11 -0
  102. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +12 -2
  103. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +0 -170
  104. package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +400 -0
  105. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +208 -0
  106. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +12 -0
  107. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  108. package/package.json +17 -14
  109. package/eventcatalog/src/pages/visualiser/context-map/index.astro +0 -30
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Placeholder div the Context Diagram graph is portaled into.
3
+ *
4
+ * Mirrors NodeGraphPortal, but uses a distinct `-context-diagram-portal` id so a
5
+ * system page can render both its System Diagram (the default `<NodeGraph />`)
6
+ * and its Context Diagram (`<ContextDiagram />`) without the two graphs fighting
7
+ * over the same portal target.
8
+ */
9
+ const ContextDiagramPortal = (props: any) => {
10
+ return (
11
+ <div
12
+ className="not-prose h-[30em] my-6 mb-12 w-full relative border! border-[rgb(var(--ec-page-border))]! rounded-md overflow-hidden"
13
+ id={`${props.id}-context-diagram-portal`}
14
+ style={{
15
+ maxHeight: props.maxHeight ? `${props.maxHeight}em` : `30em`,
16
+ }}
17
+ />
18
+ );
19
+ };
20
+
21
+ export default ContextDiagramPortal;
@@ -38,6 +38,7 @@ interface AstroNodeGraphProps {
38
38
  maxTextSize?: number;
39
39
  isDevMode?: boolean;
40
40
  resourceKey?: string;
41
+ disableMessageAnimation?: boolean;
41
42
  }
42
43
 
43
44
  const AstroNodeGraph = ({ isDevMode = false, resourceKey, ...otherProps }: AstroNodeGraphProps) => {
@@ -7,14 +7,17 @@ import {
7
7
  getNodesAndEdgesForEvents,
8
8
  getNodesAndEdgesForQueries,
9
9
  } from '@utils/node-graphs/message-node-graph';
10
- import {
11
- getNodesAndEdges as getNodesAndEdgesForDomain,
12
- getNodesAndEdgesForDomainContextMap,
13
- } from '@utils/node-graphs/domains-node-graph';
10
+ import { getNodesAndEdges as getNodesAndEdgesForDomain } from '@utils/node-graphs/domains-node-graph';
14
11
  import { getNodesAndEdges as getNodesAndEdgesForDomainEntityMap } from '@utils/node-graphs/domain-entity-map';
15
12
  import { getDomainsCanvasData } from '@utils/node-graphs/domains-canvas';
16
13
  import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
17
14
  import { getNodesAndEdges as getNodesAndEdgesForDataProduct } from '@utils/node-graphs/data-products-node-graph';
15
+ import { getNodesAndEdges as getNodesAndEdgesForSystem } from '@utils/node-graphs/systems-node-graph';
16
+ import {
17
+ getNodesAndEdges as getNodesAndEdgesForSystemContext,
18
+ getNodesAndEdgesForDomainSystems as getNodesAndEdgesForDomainSystemsContext,
19
+ getNodesAndEdgesForAllSystems as getNodesAndEdgesForSystemContextMap,
20
+ } from '@utils/node-graphs/system-context-node-graph';
18
21
  import { buildUrl } from '@utils/url-builder';
19
22
  import { getVersionFromCollection } from '@utils/collections/versions';
20
23
  import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
@@ -40,6 +43,12 @@ interface Props {
40
43
  showSearch?: boolean;
41
44
  showLegend?: boolean;
42
45
  zoomOnScroll?: boolean;
46
+ // Override the DOM portal the graph mounts into. Defaults to `${id}-portal`.
47
+ // Needed when more than one graph for the same resource is on the page
48
+ // (e.g. the System Diagram and the Context Diagram on a system page).
49
+ portalId?: string;
50
+ // Disable + hide the "Simulate Messages" animation (e.g. on the Context Diagram).
51
+ disableMessageAnimation?: boolean;
43
52
  }
44
53
 
45
54
  const {
@@ -54,6 +63,8 @@ const {
54
63
  showSearch = true,
55
64
  showLegend = true,
56
65
  zoomOnScroll = false,
66
+ portalId,
67
+ disableMessageAnimation,
57
68
  } = Astro.props;
58
69
 
59
70
  let nodes = [],
@@ -69,6 +80,7 @@ const getNodesAndEdgesFunctions = {
69
80
  flows: getNodesAndEdgesForFlows,
70
81
  containers: getNodesAndEdgesForContainer,
71
82
  'data-products': getNodesAndEdgesForDataProduct,
83
+ systems: getNodesAndEdgesForSystem,
72
84
  };
73
85
 
74
86
  let links: { label: string; url: string }[] = [];
@@ -81,6 +93,8 @@ if (collection in getNodesAndEdgesFunctions) {
81
93
  version,
82
94
  mode,
83
95
  channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat',
96
+ // The standalone System Diagram wraps the system's nodes in one parent boundary node.
97
+ wrapInSystemGroup: collection === 'systems',
84
98
  });
85
99
 
86
100
  nodes = fetchedNodes;
@@ -111,8 +125,8 @@ if (collection in getNodesAndEdgesFunctions) {
111
125
  }
112
126
  }
113
127
 
114
- if (collection === 'domain-context-map') {
115
- const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainContextMap({});
128
+ if (collection === 'system-context-map') {
129
+ const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForSystemContextMap({ mode });
116
130
  nodes = fetchedNodes;
117
131
  edges = fetchedEdges;
118
132
  }
@@ -123,6 +137,26 @@ if (collection === 'domains-canvas') {
123
137
  edges = fetchedEdges;
124
138
  }
125
139
 
140
+ if (collection === 'systems-context') {
141
+ const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForSystemContext({
142
+ id,
143
+ version,
144
+ mode,
145
+ });
146
+ nodes = fetchedNodes;
147
+ edges = fetchedEdges;
148
+ }
149
+
150
+ if (collection === 'domains-systems-context') {
151
+ const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainSystemsContext({
152
+ id,
153
+ version,
154
+ mode,
155
+ });
156
+ nodes = fetchedNodes;
157
+ edges = fetchedEdges;
158
+ }
159
+
126
160
  if (collection === 'domains-entities') {
127
161
  const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainEntityMap({
128
162
  id,
@@ -179,6 +213,8 @@ const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
179
213
  maxTextSize={config.mermaid?.maxTextSize}
180
214
  isDevMode={isDevMode()}
181
215
  resourceKey={resourceKey}
216
+ portalId={portalId}
217
+ disableMessageAnimation={disableMessageAnimation ?? collection === 'systems-context'}
182
218
  />
183
219
  </div>
184
220
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ import { ChevronDown } from 'lucide-react';
2
3
  import { LUCIDE_ICON_MAP } from '@utils/icon-map';
3
4
 
4
5
  type ActionType = 'copy' | 'cursor';
@@ -37,6 +38,7 @@ const promptText = (slotHtml ?? '')
37
38
  .trim();
38
39
 
39
40
  const cursorUrl = `https://cursor.com/link/prompt?text=${encodeURIComponent(promptText)}`;
41
+ const promptBodyId = `ec-prompt-body-${Math.random().toString(36).slice(2)}`;
40
42
  ---
41
43
 
42
44
  <ec-prompt
@@ -49,6 +51,20 @@ const cursorUrl = `https://cursor.com/link/prompt?text=${encodeURIComponent(prom
49
51
  <div class="text-sm font-medium text-[rgb(var(--ec-page-text))] leading-snug">{description}</div>
50
52
  </div>
51
53
  <div class="flex items-center gap-1.5 shrink-0">
54
+ <button
55
+ type="button"
56
+ data-prompt-toggle
57
+ aria-expanded="false"
58
+ aria-controls={promptBodyId}
59
+ class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-md border transition-all duration-150 border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))]"
60
+ >
61
+ <span data-prompt-toggle-text>Show prompt</span>
62
+ <ChevronDown
63
+ className="w-3.5 h-3.5 shrink-0 transition-transform duration-150 data-[expanded=true]:rotate-180"
64
+ data-prompt-toggle-icon
65
+ data-expanded="false"
66
+ />
67
+ </button>
52
68
  {
53
69
  showCopy && (
54
70
  <button
@@ -123,6 +139,15 @@ const cursorUrl = `https://cursor.com/link/prompt?text=${encodeURIComponent(prom
123
139
  }
124
140
  </div>
125
141
  </div>
142
+ <div
143
+ id={promptBodyId}
144
+ data-prompt-body
145
+ hidden
146
+ class="border-t border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))]/70"
147
+ >
148
+ <pre
149
+ class="m-0 max-h-96 overflow-auto whitespace-pre-wrap break-words px-4 py-3 text-sm leading-6 text-[rgb(var(--ec-page-text))]"><code>{promptText}</code></pre>
150
+ </div>
126
151
  <div hidden><slot /></div>
127
152
  </ec-prompt>
128
153
 
@@ -130,16 +155,28 @@ const cursorUrl = `https://cursor.com/link/prompt?text=${encodeURIComponent(prom
130
155
  if (!customElements.get('ec-prompt')) {
131
156
  class ECPromptElement extends HTMLElement {
132
157
  private resetTimer: number | null = null;
133
- private button: HTMLButtonElement | null = null;
158
+ private copyButton: HTMLButtonElement | null = null;
159
+ private toggleButton: HTMLButtonElement | null = null;
160
+ private promptBody: HTMLElement | null = null;
161
+ private toggleText: HTMLElement | null = null;
162
+ private toggleIcon: HTMLElement | null = null;
134
163
  private boundHandler = this.handleCopy.bind(this);
164
+ private boundToggleHandler = this.handleToggle.bind(this);
135
165
 
136
166
  connectedCallback() {
137
- this.button = this.querySelector<HTMLButtonElement>('[data-prompt-copy]');
138
- this.button?.addEventListener('click', this.boundHandler);
167
+ this.copyButton = this.querySelector<HTMLButtonElement>('[data-prompt-copy]');
168
+ this.toggleButton = this.querySelector<HTMLButtonElement>('[data-prompt-toggle]');
169
+ this.promptBody = this.querySelector<HTMLElement>('[data-prompt-body]');
170
+ this.toggleText = this.querySelector<HTMLElement>('[data-prompt-toggle-text]');
171
+ this.toggleIcon = this.querySelector<HTMLElement>('[data-prompt-toggle-icon]');
172
+
173
+ this.copyButton?.addEventListener('click', this.boundHandler);
174
+ this.toggleButton?.addEventListener('click', this.boundToggleHandler);
139
175
  }
140
176
 
141
177
  disconnectedCallback() {
142
- this.button?.removeEventListener('click', this.boundHandler);
178
+ this.copyButton?.removeEventListener('click', this.boundHandler);
179
+ this.toggleButton?.removeEventListener('click', this.boundToggleHandler);
143
180
  if (this.resetTimer != null) {
144
181
  window.clearTimeout(this.resetTimer);
145
182
  this.resetTimer = null;
@@ -158,12 +195,28 @@ const cursorUrl = `https://cursor.com/link/prompt?text=${encodeURIComponent(prom
158
195
  this.resetTimer = window.setTimeout(() => this.setCopied(false), 1500);
159
196
  }
160
197
 
198
+ private handleToggle() {
199
+ if (!this.promptBody || !this.toggleButton) return;
200
+
201
+ const expanded = this.toggleButton.getAttribute('aria-expanded') === 'true';
202
+ this.setExpanded(!expanded);
203
+ }
204
+
205
+ private setExpanded(expanded: boolean) {
206
+ if (!this.promptBody || !this.toggleButton) return;
207
+
208
+ this.promptBody.hidden = !expanded;
209
+ this.toggleButton.setAttribute('aria-expanded', expanded ? 'true' : 'false');
210
+ if (this.toggleText) this.toggleText.textContent = expanded ? 'Hide prompt' : 'Show prompt';
211
+ if (this.toggleIcon) this.toggleIcon.dataset.expanded = expanded ? 'true' : 'false';
212
+ }
213
+
161
214
  private setCopied(copied: boolean) {
162
- if (!this.button) return;
163
- this.button.dataset.copied = copied ? 'true' : 'false';
164
- this.button.setAttribute('aria-label', copied ? 'Copied to clipboard' : 'Copy prompt');
165
- const idle = this.button.querySelector<HTMLElement>('[data-prompt-copy-idle]');
166
- const done = this.button.querySelector<HTMLElement>('[data-prompt-copy-done]');
215
+ if (!this.copyButton) return;
216
+ this.copyButton.dataset.copied = copied ? 'true' : 'false';
217
+ this.copyButton.setAttribute('aria-label', copied ? 'Copied to clipboard' : 'Copy prompt');
218
+ const idle = this.copyButton.querySelector<HTMLElement>('[data-prompt-copy-idle]');
219
+ const done = this.copyButton.querySelector<HTMLElement>('[data-prompt-copy-done]');
167
220
  if (idle) idle.style.display = copied ? 'none' : '';
168
221
  if (done) done.style.display = copied ? '' : 'none';
169
222
  }
@@ -30,9 +30,11 @@ interface Props {
30
30
  | 'channel'
31
31
  | 'diagram'
32
32
  | 'container'
33
+ | 'system'
33
34
  | 'user'
34
35
  | 'team'
35
36
  | 'doc'
37
+ | 'adr'
36
38
  | 'data-product';
37
39
  version?: string;
38
40
  }
@@ -72,9 +74,11 @@ const typeLabels: Record<string, string> = {
72
74
  channel: 'Channel',
73
75
  diagram: 'Diagram',
74
76
  container: 'Container',
77
+ system: 'System',
75
78
  user: 'User',
76
79
  team: 'Team',
77
80
  doc: 'Custom Doc',
81
+ adr: 'Decision Record',
78
82
  'data-product': 'Data Product',
79
83
  };
80
84
 
@@ -106,6 +110,9 @@ const typeIcons: Record<string, string> = {
106
110
  doc: '<path stroke-linecap="round" stroke-linejoin="round" d="M14 2v4a2 2 0 002 2h4" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V7z" /><path stroke-linecap="round" stroke-linejoin="round" d="M10 9H8" /><path stroke-linecap="round" stroke-linejoin="round" d="M16 13H8" /><path stroke-linecap="round" stroke-linejoin="round" d="M16 17H8" />', // FileText
107
111
  'data-product':
108
112
  '<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25M21 7.5v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />', // Cube
113
+ system:
114
+ '<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" />', // Group (systems)
115
+ adr: '<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /><path stroke-linecap="round" stroke-linejoin="round" d="M9 7h2" />', // ClipboardList-ish (ADR)
109
116
  };
110
117
 
111
118
  const label = typeLabels[type] || typeLabels.entity;
@@ -176,7 +183,7 @@ const truncatedSummary = summary.length > maxSummaryLength ? summary.slice(0, ma
176
183
  const isVersionedResource = type !== 'doc';
177
184
 
178
185
  // Only these types have visualizers
179
- const hasVisualizer = ['agent', 'domain', 'service', 'event', 'query', 'command', 'container'].includes(type);
186
+ const hasVisualizer = ['agent', 'domain', 'service', 'event', 'query', 'command', 'container', 'system'].includes(type);
180
187
 
181
188
  // Check deprecation status
182
189
  const deprecation = resource ? getDeprecatedDetails(resource) : null;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Placeholder div the global System Context Map graph is portaled into.
3
+ *
4
+ * Mirrors NodeGraphPortal, but uses a fixed `system-context-map-portal` id so the
5
+ * homepage (and any other page) can embed the global system context map without an
6
+ * id/version. The matching <NodeGraph collection="system-context-map" /> renders the
7
+ * graph into this portal.
8
+ */
9
+ const SystemContextMapPortal = (props: any) => {
10
+ return (
11
+ <div
12
+ className="not-prose my-6 mb-12 w-full relative border! border-[rgb(var(--ec-page-border))]! rounded-md overflow-hidden"
13
+ id="system-context-map-portal"
14
+ style={{
15
+ height: props.height ? `${props.height}em` : '36em',
16
+ maxHeight: props.maxHeight ? `${props.maxHeight}em` : '36em',
17
+ }}
18
+ />
19
+ );
20
+ };
21
+
22
+ export default SystemContextMapPortal;
@@ -17,6 +17,7 @@ import ChannelInformation from '@components/MDX/ChannelInformation/ChannelInform
17
17
  import Attachments from '@components/MDX/Attachments.astro';
18
18
  import AgentTools from '@components/MDX/AgentTools/AgentTools.astro';
19
19
  import MessageTable from '@components/MDX/MessageTable/MessageTable.astro';
20
+ import ADRTable from '@components/MDX/ADRTable/ADRTable.astro';
20
21
  import ResourceGroupTable from '@components/MDX/ResourceGroupTable/ResourceGroupTable.astro';
21
22
  import EntityPropertiesTable from '@components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro';
22
23
  import Tabs from '@components/MDX/Tabs/Tabs.astro';
@@ -34,6 +35,8 @@ import MermaidFileLoader from '@components/MDX/MermaidFileLoader/MermaidFileLoad
34
35
  import LikeC4View from '@components/MDX/LikeC4View/LikeC4View.astro';
35
36
  // Portals: required for server/client components
36
37
  import NodeGraphPortal from '@components/MDX/NodeGraph/NodeGraphPortal';
38
+ import ContextDiagramPortal from '@components/MDX/ContextDiagram/ContextDiagramPortal';
39
+ import SystemContextMapPortal from '@components/MDX/SystemContextMap/SystemContextMapPortal';
37
40
  import SchemaViewerPortal from '@components/MDX/SchemaViewer/SchemaViewerPortal';
38
41
  import { jsx } from 'astro/jsx-runtime';
39
42
  import RemoteSchema from '@components/MDX/RemoteSchema.astro';
@@ -54,8 +57,12 @@ const components = (props: any) => {
54
57
  Flow,
55
58
  Link: (mdxProp: any) => jsx(Link, { ...props, ...mdxProp }),
56
59
  MessageTable: (mdxProp: any) => jsx(MessageTable, { ...props, ...mdxProp }),
60
+ ADRTable: (mdxProp: any) => jsx(ADRTable, { ...props, ...mdxProp }),
57
61
  EntityPropertiesTable: (mdxProp: any) => jsx(EntityPropertiesTable, { ...props, ...mdxProp }),
58
62
  NodeGraph: (mdxProp: any) => jsx(NodeGraphPortal, { ...props.data, ...mdxProp, props, mdxProp }),
63
+ ComponentDiagram: (mdxProp: any) => jsx(NodeGraphPortal, { ...props.data, ...mdxProp, props, mdxProp }),
64
+ ContextDiagram: (mdxProp: any) => jsx(ContextDiagramPortal, { ...props.data, ...mdxProp, props, mdxProp }),
65
+ SystemContextMap: (mdxProp: any) => jsx(SystemContextMapPortal, { ...mdxProp }),
59
66
  EntityMap: (mdxProp: any) => jsx(EntityMap, { ...props, ...mdxProp }),
60
67
  OpenAPI,
61
68
  Prompt: (mdxProp: any) => jsx(Prompt, { ...props, ...mdxProp }),
@@ -422,7 +422,7 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
422
422
  )}
423
423
  </div>
424
424
 
425
- {showFormatFilters && (
425
+ {showFormatFilters && latestMessages.length > 0 && (
426
426
  <div className="flex-shrink-0 border-b border-[rgb(var(--ec-page-border))] px-4 py-3">
427
427
  <div className="rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3">
428
428
  <div className="mb-3 flex items-center justify-between gap-3">
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useMemo } from 'react';
4
4
  import * as LucideIcons from 'lucide-react';
5
- import { ChevronRight, ChevronLeft, ChevronDown, Home, Star, FileQuestion } from 'lucide-react';
5
+ import { ChevronRight, ChevronLeft, ChevronDown, Home, Star } from 'lucide-react';
6
6
  import type { NavNode, ChildRef } from '@stores/sidebar-store/state';
7
7
  import { saveState, loadState, saveCollapsedSections, loadCollapsedSections } from './storage';
8
8
  import { useStore } from '@nanostores/react';
@@ -218,6 +218,12 @@ export default function NestedSideBar() {
218
218
  { pattern: /^\/docs\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
219
219
  { pattern: /^\/visualiser\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
220
220
  { pattern: /^\/architecture\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
221
+ // Systems (key prefix is `system`, url segment is `systems`).
222
+ // The visualiser pattern also matches the System Diagram
223
+ // (/visualiser/systems/:id/:version/context) since it is not end-anchored.
224
+ { pattern: /^\/docs\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
225
+ { pattern: /^\/visualiser\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
226
+ { pattern: /^\/architecture\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
221
227
  // Agents
222
228
  { pattern: /^\/docs\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
223
229
  { pattern: /^\/visualiser\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
@@ -584,6 +590,19 @@ export default function NestedSideBar() {
584
590
  });
585
591
  };
586
592
 
593
+ /**
594
+ * Count entries that actually resolve to a visible node. Some refs (e.g. unresolved
595
+ * `list:` aliases on an empty catalog) sit in `entries` but render nothing — those
596
+ * must not count, otherwise the empty state is suppressed and the rail looks blank.
597
+ * Mirrors the filtering in `renderEntries`.
598
+ */
599
+ const hasVisibleEntries = currentLevel.entries.some((ref) => {
600
+ const node = resolveRef(ref);
601
+ if (!isVisible(node)) return false;
602
+ if (isGroup(node!)) return hasVisibleChildren(node!);
603
+ return true;
604
+ });
605
+
587
606
  /**
588
607
  * Handle drilling down into an item with children
589
608
  */
@@ -734,37 +753,37 @@ export default function NestedSideBar() {
734
753
  const child = resolveRef(childRef);
735
754
  return child && isVisible(child);
736
755
  }) ?? [];
737
- const shouldFlattenSubtleChildren =
738
- !isSubtleGroup &&
739
- visibleChildren.length > 0 &&
740
- visibleChildren.every((childRef) => {
741
- const child = resolveRef(childRef);
742
- return !!child && isGroup(child) && child.subtle === true;
743
- });
744
756
 
745
757
  const groupId = groupKey || `group-${index}`;
746
758
  const canCollapse = visibleChildren.length > 3;
747
759
  const isCollapsed = collapsedSections.has(groupId);
748
760
 
761
+ // When a group's children are subtle subgroups (e.g. Resources > Services/Flows/Data Stores),
762
+ // they render flush under the parent icon instead of inside the indented border guide.
763
+ const hasSubtleChildren = visibleChildren.some((childRef) => {
764
+ const child = resolveRef(childRef);
765
+ return child && isGroup(child) && child.subtle === true;
766
+ });
767
+
749
768
  const headerContent = (
750
769
  <>
751
770
  <div className="flex items-center gap-2">
752
771
  {GroupIcon && (
753
772
  <span
754
773
  className={cn(
755
- 'flex items-center justify-center',
774
+ 'flex items-center justify-center w-5 h-5 rounded',
756
775
  isSubtleGroup
757
- ? 'w-4 h-4 text-[rgb(var(--ec-content-text-muted))]'
758
- : 'w-5 h-5 rounded bg-[rgb(var(--ec-group-icon-bg))] text-[rgb(var(--ec-group-icon-text))]'
776
+ ? 'bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-content-text-muted))]'
777
+ : 'bg-[rgb(var(--ec-group-icon-bg))] text-[rgb(var(--ec-group-icon-text))]'
759
778
  )}
760
779
  >
761
- <GroupIcon className={cn(isSubtleGroup ? 'w-3 h-3' : 'w-3 h-3')} />
780
+ <GroupIcon className="w-3 h-3" />
762
781
  </span>
763
782
  )}
764
783
  <span
765
784
  className={cn(
766
785
  isSubtleGroup
767
- ? 'text-[10px] font-semibold uppercase tracking-[0.12em] text-[rgb(var(--ec-content-text-muted))]'
786
+ ? 'text-[9px] font-semibold uppercase tracking-[0.1em] text-[rgb(var(--ec-content-text-muted))]'
768
787
  : 'text-[12px] font-semibold tracking-tight text-[rgb(var(--ec-content-text))]'
769
788
  )}
770
789
  >
@@ -791,14 +810,14 @@ export default function NestedSideBar() {
791
810
  className={cn(
792
811
  'flex items-center justify-between w-full rounded-md transition-colors cursor-pointer',
793
812
  isSubtleGroup
794
- ? 'px-1.5 py-1 hover:bg-[rgb(var(--ec-content-hover))]/60'
813
+ ? 'px-2 py-1 hover:bg-[rgb(var(--ec-content-hover))]/60'
795
814
  : 'px-2 py-1.5 hover:bg-[rgb(var(--ec-content-hover))]'
796
815
  )}
797
816
  >
798
817
  {headerContent}
799
818
  </button>
800
819
  ) : (
801
- <div className={cn('flex items-center justify-between', isSubtleGroup ? 'px-1.5 py-1' : 'px-2 py-1.5')}>
820
+ <div className={cn('flex items-center justify-between', isSubtleGroup ? 'px-2 py-1' : 'px-2 py-1.5')}>
802
821
  {headerContent}
803
822
  </div>
804
823
  )}
@@ -806,7 +825,7 @@ export default function NestedSideBar() {
806
825
  <div
807
826
  className={cn(
808
827
  'flex flex-col gap-0.5 border-[rgb(var(--ec-content-border))]',
809
- isSubtleGroup ? 'mt-0.5' : shouldFlattenSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
828
+ isSubtleGroup ? 'border-l ml-4 mt-1' : hasSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
810
829
  )}
811
830
  >
812
831
  {visibleChildren.map((childRef, childIndex) => {
@@ -822,19 +841,16 @@ export default function NestedSideBar() {
822
841
  return (
823
842
  <div
824
843
  key={`nested-group-${childKey || childIndex}`}
825
- className={cn(
826
- child.subtle
827
- ? shouldFlattenSubtleChildren
828
- ? 'ml-5 mt-0.5'
829
- : 'ml-1.5 mt-1 pl-1.5'
830
- : 'ml-3 mt-1.5 pl-3 border-l border-[rgb(var(--ec-content-border))]'
831
- )}
844
+ className={cn(child.subtle ? 'mt-1' : 'ml-3 mt-1.5 pl-3 border-l border-[rgb(var(--ec-content-border))]')}
832
845
  >
833
846
  {renderGroup(child, childKey, childIndex)}
834
847
  </div>
835
848
  );
836
849
  }
837
- return renderItem(child, childKey, childIndex);
850
+ // Inside a subtle subgroup (e.g. a domain's Resources > Services list) the
851
+ // section header already conveys the resource type, so suppress the default
852
+ // per-item icon and only keep a custom icon if one is defined.
853
+ return renderItem(child, childKey, childIndex, isSubtleGroup);
838
854
  })}
839
855
  </div>
840
856
  )}
@@ -845,14 +861,22 @@ export default function NestedSideBar() {
845
861
  /**
846
862
  * Render a single item
847
863
  */
848
- const renderItem = (item: NavNode, itemKey: string | null, index: number) => {
864
+ const renderItem = (item: NavNode, itemKey: string | null, index: number, suppressDefaultIcon = false) => {
849
865
  const itemHasChildren = hasChildren(item);
850
866
  const isActive = urlsMatch(item.href, currentPath) || (itemKey !== null && itemKey === activeNodeKey);
851
867
  const isFav = isFavorited(itemKey);
852
868
  const canFavorite = itemKey !== null; // Only items with keys can be favorited
853
869
 
854
- // Get icon component from lucide-react
855
- const IconComponent = item.icon ? (LucideIcons as unknown as Record<string, LucideIcons.LucideIcon>)[item.icon] : null;
870
+ // Get icon component from lucide-react. When suppressDefaultIcon is set (e.g. items
871
+ // listed under a typed subtle subgroup) the default per-collection glyph is hidden;
872
+ // a custom icon (item.leftIcon) is always still shown.
873
+ const IconComponent =
874
+ item.icon && !suppressDefaultIcon ? (LucideIcons as unknown as Record<string, LucideIcons.LucideIcon>)[item.icon] : null;
875
+ // Some glyphs fill their box more than others, so they read larger at the shared
876
+ // item size. Render these ~25% smaller (matching the ADR icon) to visually balance:
877
+ // ClipboardList (ADRs) and the message glyphs (events/commands/queries).
878
+ const smallerIcons = ['ClipboardList', 'Zap', 'MessageSquare', 'Search', 'Mail'];
879
+ const iconSizeClass = item.icon && smallerIcons.includes(item.icon) ? 'w-3 h-3' : 'w-4 h-4';
856
880
 
857
881
  const handleStarClick = (e: React.MouseEvent) => {
858
882
  e.preventDefault();
@@ -870,7 +894,7 @@ export default function NestedSideBar() {
870
894
  isActive ? 'text-[rgb(var(--ec-accent-text))]' : 'text-[rgb(var(--ec-content-text-muted))]'
871
895
  )}
872
896
  >
873
- <IconComponent className="w-4 h-4" />
897
+ <IconComponent className={iconSizeClass} />
874
898
  </span>
875
899
  )}
876
900
  {item.leftIcon && <img src={resolveIconUrl(item.leftIcon)} alt="" loading="lazy" className="w-4 h-4 flex-shrink-0" />}
@@ -1183,19 +1207,25 @@ export default function NestedSideBar() {
1183
1207
  )}
1184
1208
 
1185
1209
  {/* Empty State */}
1186
- {currentLevel.entries.length === 0 && favorites.length === 0 && (
1187
- <div className="flex flex-col items-center justify-center px-6 py-12 text-center">
1188
- <div className="mb-4 p-3 rounded-full bg-[rgb(var(--ec-group-icon-bg))]">
1189
- <FileQuestion className="w-8 h-8 text-[rgb(var(--ec-icon-color))]" />
1190
- </div>
1191
- <h3 className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] mb-2">Your catalog is empty</h3>
1192
- <p className="text-[11px] text-[rgb(var(--ec-content-text-muted))] leading-relaxed max-w-[240px]">
1193
- Navigation will appear here when you add resources to your EventCatalog.
1210
+ {!hasVisibleEntries && favorites.length === 0 && (
1211
+ <div className="flex min-h-full flex-col items-center justify-center p-8 text-center">
1212
+ <h3 className="mb-1 text-sm font-semibold text-[rgb(var(--ec-page-text))]">Your catalog is empty</h3>
1213
+ <p className="max-w-[220px] text-sm font-light leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
1214
+ Navigation will appear here when you add resources.{' '}
1215
+ <a
1216
+ href="https://www.eventcatalog.dev/docs/tutorial/install-eventcatalog"
1217
+ target="_blank"
1218
+ rel="noopener noreferrer"
1219
+ className="font-medium text-[rgb(var(--ec-accent))] hover:underline"
1220
+ >
1221
+ Add your first resource
1222
+ </a>
1223
+ .
1194
1224
  </p>
1195
1225
  </div>
1196
1226
  )}
1197
1227
 
1198
- {currentLevel.entries.length > 0 && renderEntries(currentLevel.entries)}
1228
+ {hasVisibleEntries && renderEntries(currentLevel.entries)}
1199
1229
  </nav>
1200
1230
 
1201
1231
  {/* Animation keyframes */}