@eventcatalog/core 3.0.0-beta.8 → 3.0.0

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 (148) hide show
  1. package/README.md +41 -98
  2. package/dist/__mocks__/astro-content.cjs +32 -0
  3. package/dist/__mocks__/astro-content.d.cts +13 -0
  4. package/dist/__mocks__/astro-content.d.ts +13 -0
  5. package/dist/__mocks__/astro-content.js +7 -0
  6. package/dist/analytics/analytics.cjs +1 -1
  7. package/dist/analytics/analytics.js +2 -2
  8. package/dist/analytics/log-build.cjs +1 -1
  9. package/dist/analytics/log-build.js +3 -3
  10. package/dist/catalog-to-astro-content-directory.cjs +2 -19
  11. package/dist/catalog-to-astro-content-directory.d.cts +1 -2
  12. package/dist/catalog-to-astro-content-directory.d.ts +1 -2
  13. package/dist/catalog-to-astro-content-directory.js +3 -5
  14. package/dist/{chunk-R2BJ7MJG.js → chunk-6Z6ARMQS.js} +1 -17
  15. package/dist/{chunk-LQUXA3NB.js → chunk-BYP43AAT.js} +1 -1
  16. package/dist/{chunk-UTHNQFM7.js → chunk-E5Q7TZYT.js} +1 -1
  17. package/dist/{chunk-KEYJ3FB3.js → chunk-EKGR533N.js} +1 -1
  18. package/dist/{chunk-7MCE4J6I.js → chunk-KF5PARQK.js} +1 -1
  19. package/dist/{chunk-I3QUYHIK.js → chunk-VO5WYA44.js} +1 -1
  20. package/dist/constants.cjs +1 -1
  21. package/dist/constants.js +1 -1
  22. package/dist/eventcatalog.cjs +20 -64
  23. package/dist/eventcatalog.config.d.cts +4 -0
  24. package/dist/eventcatalog.config.d.ts +4 -0
  25. package/dist/eventcatalog.js +26 -52
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/utils/cli-logger.cjs +1 -1
  29. package/dist/utils/cli-logger.js +2 -2
  30. package/eventcatalog/astro.config.mjs +4 -1
  31. package/eventcatalog/integrations/eventcatalog-features.ts +69 -0
  32. package/eventcatalog/public/icons/asyncapi-black.svg +2 -0
  33. package/eventcatalog/public/icons/graphql-black.svg +1 -0
  34. package/eventcatalog/public/icons/openapi-black.svg +1 -0
  35. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +994 -0
  36. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +24 -0
  37. package/eventcatalog/src/components/Grids/DomainGrid.tsx +310 -173
  38. package/eventcatalog/src/components/Grids/MessageGrid.tsx +299 -180
  39. package/eventcatalog/src/components/Grids/specification-utils.ts +106 -0
  40. package/eventcatalog/src/components/Header.astro +25 -5
  41. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +14 -3
  42. package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +95 -90
  43. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +144 -0
  44. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +34 -8
  45. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -2
  46. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +140 -109
  47. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +5 -14
  48. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +247 -59
  49. package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +64 -126
  50. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +41 -43
  51. package/eventcatalog/src/components/Search/Search.astro +2 -2
  52. package/eventcatalog/src/components/Search/SearchDataLoader.astro +25 -0
  53. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +6 -3
  54. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +44 -16
  55. package/eventcatalog/src/components/SideNav/SideNav.astro +0 -15
  56. package/eventcatalog/src/components/Tables/Table.tsx +96 -77
  57. package/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +108 -74
  58. package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +74 -55
  59. package/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +36 -36
  60. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +110 -77
  61. package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +105 -94
  62. package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +31 -26
  63. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +115 -215
  64. package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +145 -243
  65. package/eventcatalog/src/content.config.ts +1 -13
  66. package/eventcatalog/src/enterprise/ai/chat-api.ts +360 -0
  67. package/eventcatalog/src/enterprise/auth/[...auth].ts +3 -0
  68. package/eventcatalog/src/enterprise/auth/login.astro +420 -0
  69. package/eventcatalog/src/enterprise/collections/index.ts +0 -1
  70. package/eventcatalog/src/layouts/Footer.astro +8 -5
  71. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +133 -117
  72. package/eventcatalog/src/pages/_index.astro +243 -559
  73. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +8 -2
  74. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +9 -5
  75. package/eventcatalog/src/pages/directory/[type]/index.astro +6 -0
  76. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -3
  77. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +7 -7
  78. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
  79. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +10 -7
  80. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +194 -121
  81. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +94 -70
  82. package/eventcatalog/src/pages/docs/teams/[id].mdx.ts +36 -0
  83. package/eventcatalog/src/pages/docs/users/[id]/index.astro +56 -45
  84. package/eventcatalog/src/pages/docs/users/[id].mdx.ts +36 -0
  85. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +178 -0
  86. package/eventcatalog/src/pages/schemas/explorer/index.astro +7 -157
  87. package/eventcatalog/src/pages/studio.astro +124 -72
  88. package/eventcatalog/src/remark-plugins/directives.ts +30 -9
  89. package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/container.ts +10 -1
  90. package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/domain.ts +17 -7
  91. package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/message.ts +10 -1
  92. package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/service.ts +11 -4
  93. package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/shared.ts +14 -0
  94. package/eventcatalog/src/stores/{sidebar-store.ts → sidebar-store/index.ts} +1 -1
  95. package/eventcatalog/src/utils/collections/channels.ts +0 -2
  96. package/eventcatalog/src/utils/collections/commands.ts +0 -2
  97. package/eventcatalog/src/utils/collections/containers.ts +0 -2
  98. package/eventcatalog/src/utils/collections/domains.ts +0 -2
  99. package/eventcatalog/src/utils/collections/entities.ts +0 -2
  100. package/eventcatalog/src/utils/collections/events.ts +0 -2
  101. package/eventcatalog/src/utils/collections/flows.ts +0 -2
  102. package/eventcatalog/src/utils/collections/queries.ts +0 -2
  103. package/eventcatalog/src/utils/collections/schemas.ts +45 -7
  104. package/eventcatalog/src/utils/collections/services.ts +0 -2
  105. package/eventcatalog/src/utils/feature.ts +9 -5
  106. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +1 -1
  107. package/eventcatalog/src/utils/resource-files.ts +86 -0
  108. package/package.json +12 -15
  109. package/default-files-for-collections/changelogs.md +0 -5
  110. package/default-files-for-collections/channels.md +0 -8
  111. package/default-files-for-collections/commands.md +0 -8
  112. package/default-files-for-collections/domains.md +0 -8
  113. package/default-files-for-collections/events.md +0 -8
  114. package/default-files-for-collections/flows.md +0 -11
  115. package/default-files-for-collections/queries.md +0 -8
  116. package/default-files-for-collections/services.md +0 -8
  117. package/default-files-for-collections/ubiquitousLanguages.md +0 -7
  118. package/eventcatalog/src/enterprise/collections/chat-prompts.ts +0 -32
  119. package/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +0 -60
  120. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +0 -414
  121. package/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatSidebar.tsx +0 -169
  122. package/eventcatalog/src/enterprise/eventcatalog-chat/components/InputModal.tsx +0 -244
  123. package/eventcatalog/src/enterprise/eventcatalog-chat/components/MentionInput.tsx +0 -211
  124. package/eventcatalog/src/enterprise/eventcatalog-chat/components/WelcomePromptArea.tsx +0 -176
  125. package/eventcatalog/src/enterprise/eventcatalog-chat/components/default-prompts.ts +0 -93
  126. package/eventcatalog/src/enterprise/eventcatalog-chat/components/hooks/ChatProvider.tsx +0 -143
  127. package/eventcatalog/src/enterprise/eventcatalog-chat/components/windows/ChatWindow.server.tsx +0 -387
  128. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/api/chat.ts +0 -59
  129. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +0 -104
  130. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/ai-provider.ts +0 -140
  131. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts +0 -28
  132. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts +0 -41
  133. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts +0 -26
  134. package/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts +0 -61
  135. package/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +0 -50
  136. package/eventcatalog/src/pages/auth/login.astro +0 -280
  137. package/eventcatalog/src/pages/chat/feature.astro +0 -179
  138. package/eventcatalog/src/pages/chat/index.astro +0 -10
  139. package/eventcatalog/src/pages/docs/_default-docs.mdx +0 -25
  140. package/eventcatalog/src/pages/docs/index.astro +0 -33
  141. package/eventcatalog/src/pages/nav-index.json.ts +0 -30
  142. /package/eventcatalog/src/{pages → enterprise}/auth/error.astro +0 -0
  143. /package/eventcatalog/src/{middleware-auth.ts → enterprise/auth/middleware/middleware-auth.ts} +0 -0
  144. /package/eventcatalog/src/{middleware.ts → enterprise/auth/middleware/middleware.ts} +0 -0
  145. /package/eventcatalog/src/{pages/unauthorized/index.astro → enterprise/auth/unauthorized.astro} +0 -0
  146. /package/eventcatalog/src/{pages → enterprise}/plans/index.astro +0 -0
  147. /package/eventcatalog/src/{components/SideNav/NestedSideBar → stores/sidebar-store}/builders/flow.ts +0 -0
  148. /package/eventcatalog/src/{components/SideNav/NestedSideBar/sidebar-builder.ts → stores/sidebar-store/state.ts} +0 -0
@@ -8,10 +8,13 @@ import {
8
8
  CircleStackIcon,
9
9
  ChevronDownIcon,
10
10
  ChevronUpIcon,
11
- ArrowsPointingOutIcon,
11
+ ArrowTopRightOnSquareIcon,
12
+ ArrowLongRightIcon,
13
+ ArrowLongLeftIcon,
12
14
  } from '@heroicons/react/24/outline';
13
15
  import { buildUrl } from '@utils/url-builder';
14
- import { BoxIcon, ArrowRight, ArrowLeft } from 'lucide-react';
16
+ import { BoxIcon } from 'lucide-react';
17
+ import { getSpecUrl, getSpecIcon, getSpecLabel, getServiceSpecifications } from './specification-utils';
15
18
 
16
19
  // ============================================
17
20
  // Types
@@ -32,14 +35,14 @@ const getMessageIcon = (collection: string) => {
32
35
  case 'commands':
33
36
  return { Icon: ChatBubbleLeftIcon, color: 'blue' };
34
37
  case 'queries':
35
- return { Icon: MagnifyingGlassIcon, color: 'green' };
38
+ return { Icon: MagnifyingGlassIcon, color: 'emerald' };
36
39
  default:
37
40
  return { Icon: BoltIcon, color: 'gray' };
38
41
  }
39
42
  };
40
43
 
41
44
  // ============================================
42
- // Simple Sub-components
45
+ // Sub-components
43
46
  // ============================================
44
47
 
45
48
  const EntityBadge = memo(({ entity }: { entity: any }) => {
@@ -49,177 +52,269 @@ const EntityBadge = memo(({ entity }: { entity: any }) => {
49
52
  return (
50
53
  <a
51
54
  href={buildUrl(`/docs/entities/${id}`)}
52
- className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-purple-100 border border-purple-300 rounded-md text-xs font-medium hover:bg-purple-200 transition-colors"
55
+ className="inline-flex items-center gap-2 px-3 py-2 bg-white border border-gray-200 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm"
53
56
  >
54
- <BoxIcon className="h-3.5 w-3.5 text-purple-600" />
55
- <span className="text-purple-800">{name}</span>
57
+ <BoxIcon className="h-4 w-4 text-purple-500" />
58
+ <span>{name}</span>
56
59
  </a>
57
60
  );
58
61
  });
59
62
 
60
- const MessageBadge = memo(({ message }: { message: any }) => {
63
+ const MessageLink = memo(({ message }: { message: any }) => {
61
64
  const data = message?.data || message;
62
65
  const collection = message?.collection || 'events';
63
66
  const { Icon, color } = getMessageIcon(collection);
64
67
  const id = data?.id || message?.id;
65
68
  const name = data?.name || data?.id || id;
66
- const version = data?.version;
69
+ const version = data?.version || message?.data?.version || 'latest';
70
+
71
+ const iconStyles: Record<string, string> = {
72
+ orange: 'text-orange-500',
73
+ blue: 'text-blue-500',
74
+ emerald: 'text-emerald-500',
75
+ gray: 'text-gray-500',
76
+ };
67
77
 
68
78
  return (
69
79
  <a
70
80
  href={buildUrl(`/docs/${collection}/${id}/${version}`)}
71
- className="flex items-center gap-1.5 px-2 py-1 bg-white border border-gray-200 rounded text-[11px] font-medium hover:bg-gray-50 transition-colors"
81
+ className="flex items-center gap-2 py-1.5 text-sm text-gray-700 hover:text-gray-900 transition-colors group"
72
82
  >
73
- <Icon className={`h-3 w-3 text-${color}-500`} />
74
- <span className="text-gray-700 truncate max-w-[120px]">{name}</span>
83
+ <Icon className={`h-4 w-4 flex-shrink-0 ${iconStyles[color]}`} />
84
+ <span className="group-hover:underline">{name}</span>
85
+ <span className="text-xs text-gray-400">v{version}</span>
75
86
  </a>
76
87
  );
77
88
  });
78
89
 
79
- const ContainerBadge = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => {
90
+ const SpecificationBadge = memo(
91
+ ({ spec, serviceId, serviceVersion }: { spec: any; serviceId: string; serviceVersion: string }) => {
92
+ return (
93
+ <a
94
+ href={getSpecUrl(spec, serviceId, serviceVersion)}
95
+ className="inline-flex items-center gap-1.5 px-2 py-1.5 bg-white border border-gray-200 rounded-lg text-xs font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm"
96
+ >
97
+ <img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt={`${spec.type} icon`} className="h-3.5 w-3.5" />
98
+ <span>{getSpecLabel(spec.type)}</span>
99
+ </a>
100
+ );
101
+ }
102
+ );
103
+
104
+ const ContainerLink = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => {
80
105
  const data = container?.data || container;
81
106
  const id = data?.id || container?.id;
82
107
  const name = data?.name || id;
83
- const version = data?.version;
84
- const colorClass = type === 'reads' ? 'orange' : 'purple';
108
+ const version = data?.version || 'latest';
85
109
 
86
110
  return (
87
111
  <a
88
112
  href={buildUrl(`/docs/containers/${id}/${version}`)}
89
- className={`inline-flex items-center gap-1.5 px-2 py-1 bg-${colorClass}-100 border border-${colorClass}-300 rounded text-[11px] font-medium hover:bg-${colorClass}-200 transition-colors`}
113
+ className="flex items-center gap-2 py-1.5 text-sm text-gray-700 hover:text-gray-900 transition-colors group"
90
114
  >
91
- <CircleStackIcon className={`h-3 w-3 text-${colorClass}-600`} />
92
- <span className={`text-${colorClass}-800`}>{name}</span>
115
+ <CircleStackIcon className={`h-4 w-4 ${type === 'reads' ? 'text-amber-500' : 'text-violet-500'}`} />
116
+ <span className="group-hover:underline">{name}</span>
93
117
  </a>
94
118
  );
95
119
  });
96
120
 
121
+ // Searchable scrollable box component
122
+ const SearchableBox = memo(
123
+ ({
124
+ title,
125
+ icon: Icon,
126
+ iconColor,
127
+ items,
128
+ renderItem,
129
+ emptyText = '—',
130
+ }: {
131
+ title: string;
132
+ icon: any;
133
+ iconColor: string;
134
+ items: any[];
135
+ renderItem: (item: any, idx: number) => React.ReactNode;
136
+ emptyText?: string;
137
+ }) => {
138
+ const [search, setSearch] = useState('');
139
+
140
+ const filteredItems = items.filter((item) => {
141
+ if (!search) return true;
142
+ const data = item?.data || item;
143
+ const name = data?.name || data?.id || '';
144
+ return name.toLowerCase().includes(search.toLowerCase());
145
+ });
146
+
147
+ return (
148
+ <div className="flex flex-col">
149
+ <div className="flex items-center gap-2 mb-2">
150
+ <div className="flex items-center gap-1.5 flex-shrink-0">
151
+ <Icon className={`h-4 w-4 ${iconColor}`} />
152
+ <h4 className="text-xs font-semibold text-gray-700 uppercase tracking-wide">{title}</h4>
153
+ <span className="text-[10px] text-gray-400 font-medium">({items.length})</span>
154
+ </div>
155
+ {items.length > 0 && (
156
+ <input
157
+ type="text"
158
+ placeholder="Search..."
159
+ value={search}
160
+ onChange={(e) => setSearch(e.target.value)}
161
+ className="flex-1 px-2 py-0.5 text-xs border border-gray-200 rounded focus:outline-none focus:border-gray-300"
162
+ onClick={(e) => e.stopPropagation()}
163
+ />
164
+ )}
165
+ </div>
166
+ {items.length > 0 ? (
167
+ <div className="space-y-0.5 max-h-32 overflow-y-auto pr-1">
168
+ {filteredItems.length > 0 ? (
169
+ filteredItems.map((item, idx) => renderItem(item, idx))
170
+ ) : (
171
+ <p className="text-xs text-gray-400 italic">No matches</p>
172
+ )}
173
+ </div>
174
+ ) : (
175
+ <p className="text-xs text-gray-300">{emptyText}</p>
176
+ )}
177
+ </div>
178
+ );
179
+ }
180
+ );
181
+
182
+ // Expanded content for service card
183
+ const ServiceExpandedContent = memo(
184
+ ({ receives, sends, readsFrom, writesTo }: { receives: any[]; sends: any[]; readsFrom: any[]; writesTo: any[] }) => {
185
+ const hasMessages = receives.length > 0 || sends.length > 0;
186
+ const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
187
+
188
+ return (
189
+ <div className="border-t border-gray-100 px-4 py-3 space-y-4">
190
+ {/* Messages Row */}
191
+ {hasMessages && (
192
+ <div className="grid grid-cols-2 gap-x-6">
193
+ <SearchableBox
194
+ title="Receives"
195
+ icon={ArrowLongRightIcon}
196
+ iconColor="text-blue-400"
197
+ items={receives}
198
+ renderItem={(msg, idx) => {
199
+ const msgId = msg?.data?.id || msg?.id;
200
+ return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
201
+ }}
202
+ />
203
+ <SearchableBox
204
+ title="Sends"
205
+ icon={ArrowLongLeftIcon}
206
+ iconColor="text-emerald-400 rotate-180"
207
+ items={sends}
208
+ renderItem={(msg, idx) => {
209
+ const msgId = msg?.data?.id || msg?.id;
210
+ return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
211
+ }}
212
+ />
213
+ </div>
214
+ )}
215
+
216
+ {/* Data Row */}
217
+ {hasContainers && (
218
+ <div className="grid grid-cols-2 gap-x-6 pt-3 border-t border-gray-100">
219
+ <SearchableBox
220
+ title="Reads"
221
+ icon={CircleStackIcon}
222
+ iconColor="text-amber-400"
223
+ items={readsFrom}
224
+ renderItem={(container, idx) => {
225
+ const containerId = container?.data?.id || container?.id;
226
+ return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
227
+ }}
228
+ />
229
+ <SearchableBox
230
+ title="Writes"
231
+ icon={CircleStackIcon}
232
+ iconColor="text-violet-400"
233
+ items={writesTo}
234
+ renderItem={(container, idx) => {
235
+ const containerId = container?.data?.id || container?.id;
236
+ return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="writes" /> : null;
237
+ }}
238
+ />
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ }
244
+ );
245
+
97
246
  const ServiceCard = memo(({ service }: { service: any }) => {
98
247
  const data = service?.data || service;
248
+ const [isCollapsed, setIsCollapsed] = useState(true);
249
+
99
250
  if (!data?.id) return null;
100
251
 
101
252
  const receives = data.receives || [];
102
253
  const sends = data.sends || [];
103
254
  const readsFrom = data.readsFrom || [];
104
255
  const writesTo = data.writesTo || [];
256
+ const specifications = getServiceSpecifications(data);
105
257
  const hasMessages = receives.length > 0 || sends.length > 0;
106
258
  const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
259
+ const hasSpecs = specifications.length > 0;
260
+ const hasContent = hasMessages || hasContainers;
107
261
 
108
262
  return (
109
- <div className="bg-white border-2 border-dashed border-pink-400 rounded-lg p-4">
263
+ <div className="bg-white border border-gray-200 rounded-xl shadow-sm hover:shadow-md transition-shadow">
110
264
  {/* Service Header */}
111
- <div className="flex items-center justify-between">
112
- <a
113
- href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
114
- className="flex items-center gap-2 hover:underline"
115
- >
116
- <ServerIcon className="h-5 w-5 text-pink-500" />
117
- <span className="font-semibold text-gray-900">{data.name || data.id}</span>
118
- <span className="text-xs text-gray-500">v{data.version}</span>
119
- </a>
120
- <a
121
- href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
122
- className="p-1 hover:bg-pink-100 rounded-md transition-colors duration-200"
123
- title="Expand service architecture"
124
- onClick={(e) => e.stopPropagation()}
125
- >
126
- <ArrowsPointingOutIcon className="h-4 w-4 text-gray-500 hover:text-pink-600" />
127
- </a>
128
- </div>
129
-
130
- {data.summary && <p className="mt-2 text-sm text-gray-600 line-clamp-2">{data.summary}</p>}
131
-
132
- {/* Message Flow Diagram */}
133
- {hasMessages && (
134
- <div className="mt-4 flex items-stretch gap-3">
135
- {/* Receives (Inbound) */}
136
- <div className="flex-1 bg-blue-50 border border-blue-200 rounded-lg p-3">
137
- <div className="flex items-center gap-1.5 mb-2">
138
- <ArrowRight className="h-3.5 w-3.5 text-blue-500" />
139
- <span className="text-xs font-semibold text-blue-700 uppercase">Inbound Messages</span>
140
- <span className="text-xs text-blue-500">({receives.length})</span>
141
- </div>
142
- {receives.length > 0 ? (
143
- <div className="space-y-1.5">
144
- {receives.slice(0, 4).map((msg: any, idx: number) => {
145
- const msgId = msg?.data?.id || msg?.id;
146
- return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
147
- })}
148
- {receives.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{receives.length - 4} more</p>}
149
- </div>
150
- ) : (
151
- <p className="text-[10px] text-gray-400 italic">No incoming messages</p>
152
- )}
265
+ <div
266
+ onClick={() => hasContent && setIsCollapsed(!isCollapsed)}
267
+ className={`flex items-center justify-between px-4 py-3 ${hasContent ? 'cursor-pointer hover:bg-gray-50' : ''} transition-colors`}
268
+ >
269
+ <div className="flex items-center gap-2.5">
270
+ <div className="flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg">
271
+ <ServerIcon className="h-4 w-4 text-pink-600" />
153
272
  </div>
154
-
155
- {/* Service Icon (Center) */}
156
- <div className="flex items-center">
157
- <div className="bg-pink-100 border-2 border-pink-300 rounded-lg p-3">
158
- <ServerIcon className="h-6 w-6 text-pink-500" />
273
+ <div>
274
+ <div className="flex items-center gap-2">
275
+ <span className="font-semibold text-gray-900">{data.name || data.id}</span>
276
+ <span className="text-[11px] text-gray-500 font-medium bg-gray-100 px-1.5 py-0.5 rounded">v{data.version}</span>
159
277
  </div>
160
- </div>
161
-
162
- {/* Sends (Outbound) */}
163
- <div className="flex-1 bg-green-50 border border-green-200 rounded-lg p-3">
164
- <div className="flex items-center gap-1.5 mb-2">
165
- <ArrowLeft className="h-3.5 w-3.5 text-green-500" />
166
- <span className="text-xs font-semibold text-green-700 uppercase">Outbound Messages</span>
167
- <span className="text-xs text-green-500">({sends.length})</span>
168
- </div>
169
- {sends.length > 0 ? (
170
- <div className="space-y-1.5">
171
- {sends.slice(0, 4).map((msg: any, idx: number) => {
172
- const msgId = msg?.data?.id || msg?.id;
173
- return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
174
- })}
175
- {sends.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{sends.length - 4} more</p>}
176
- </div>
177
- ) : (
178
- <p className="text-[10px] text-gray-400 italic">No outgoing messages</p>
179
- )}
278
+ {data.summary && <p className="text-xs text-gray-500 line-clamp-1 mt-0.5 max-w-md">{data.summary}</p>}
180
279
  </div>
181
280
  </div>
182
- )}
183
-
184
- {/* Container Relationships */}
185
- {hasContainers && (
186
- <div className="mt-4 pt-4 border-t border-gray-200 grid grid-cols-2 gap-4">
187
- {/* Reads From */}
188
- {readsFrom.length > 0 && (
189
- <div>
190
- <div className="flex items-center gap-1.5 mb-2">
191
- <CircleStackIcon className="h-3.5 w-3.5 text-orange-500" />
192
- <span className="text-xs font-semibold text-gray-700">Reads from</span>
193
- </div>
194
- <div className="flex flex-wrap gap-1.5">
195
- {readsFrom.slice(0, 3).map((container: any, idx: number) => {
196
- const containerId = container?.data?.id || container?.id;
197
- return containerId ? <ContainerBadge key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
198
- })}
199
- {readsFrom.length > 3 && <span className="text-[10px] text-gray-500">+{readsFrom.length - 3} more</span>}
200
- </div>
281
+ <div className="flex items-center gap-2">
282
+ {/* Specs in header - always visible */}
283
+ {hasSpecs && (
284
+ <div className="flex items-center gap-1.5">
285
+ {specifications.map((spec: any, idx: number) => (
286
+ <a
287
+ key={`${spec.type}-${idx}`}
288
+ href={getSpecUrl(spec, data.id, data.version)}
289
+ onClick={(e) => e.stopPropagation()}
290
+ className="flex items-center gap-1 px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded text-xs text-gray-600 hover:text-gray-900 transition-colors"
291
+ title={getSpecLabel(spec.type)}
292
+ >
293
+ <img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt="" className="h-3.5 w-3.5" />
294
+ <span className="hidden sm:inline">{getSpecLabel(spec.type)}</span>
295
+ </a>
296
+ ))}
201
297
  </div>
202
298
  )}
203
-
204
- {/* Writes To */}
205
- {writesTo.length > 0 && (
206
- <div>
207
- <div className="flex items-center gap-1.5 mb-2">
208
- <CircleStackIcon className="h-3.5 w-3.5 text-purple-500" />
209
- <span className="text-xs font-semibold text-gray-700">Writes to</span>
210
- </div>
211
- <div className="flex flex-wrap gap-1.5">
212
- {writesTo.slice(0, 3).map((container: any, idx: number) => {
213
- const containerId = container?.data?.id || container?.id;
214
- return containerId ? (
215
- <ContainerBadge key={`${containerId}-${idx}`} container={container} type="writes" />
216
- ) : null;
217
- })}
218
- {writesTo.length > 3 && <span className="text-[10px] text-gray-500">+{writesTo.length - 3} more</span>}
219
- </div>
299
+ {hasContent && (
300
+ <div className="p-1.5 text-gray-400">
301
+ {isCollapsed ? <ChevronDownIcon className="h-4 w-4" /> : <ChevronUpIcon className="h-4 w-4" />}
220
302
  </div>
221
303
  )}
304
+ <a
305
+ href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
306
+ onClick={(e) => e.stopPropagation()}
307
+ className="p-1.5 text-gray-400 hover:text-pink-600 hover:bg-pink-50 rounded-lg transition-colors"
308
+ title="View service architecture"
309
+ >
310
+ <ArrowTopRightOnSquareIcon className="h-4 w-4" />
311
+ </a>
222
312
  </div>
313
+ </div>
314
+
315
+ {/* Expanded Content - Compact Flow */}
316
+ {!isCollapsed && hasContent && (
317
+ <ServiceExpandedContent receives={receives} sends={sends} readsFrom={readsFrom} writesTo={writesTo} />
223
318
  )}
224
319
  </div>
225
320
  );
@@ -227,7 +322,7 @@ const ServiceCard = memo(({ service }: { service: any }) => {
227
322
 
228
323
  const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
229
324
  const data = subdomain?.data || subdomain;
230
- const [isCollapsed, setIsCollapsed] = useState(false);
325
+ const [isCollapsed, setIsCollapsed] = useState(true);
231
326
 
232
327
  if (!data?.id) return null;
233
328
 
@@ -235,39 +330,59 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
235
330
  const entities = data.entities || [];
236
331
 
237
332
  return (
238
- <div className="bg-orange-50 border-2 border-orange-400 rounded-lg p-6">
239
- {/* Subdomain Header */}
240
- <div className="flex items-center justify-between mb-4">
241
- <div className="flex items-center gap-2">
242
- <RectangleGroupIcon className="h-5 w-5 text-orange-500" />
243
- <h3 className="text-lg font-semibold text-gray-900">{data.name || data.id}</h3>
244
- <span className="text-xs text-gray-500">v{data.version}</span>
245
- <span className="px-2 py-0.5 bg-orange-200 text-orange-800 text-xs rounded">Subdomain</span>
333
+ <div className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
334
+ {/* Subdomain Header - Clickable */}
335
+ <div
336
+ onClick={() => setIsCollapsed(!isCollapsed)}
337
+ className={`flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-gray-50 transition-colors ${!isCollapsed ? 'border-b border-gray-200' : ''}`}
338
+ >
339
+ <div className="flex items-center gap-3">
340
+ <div className="flex items-center justify-center w-9 h-9 bg-orange-100 rounded-lg">
341
+ <RectangleGroupIcon className="h-5 w-5 text-orange-600" />
342
+ </div>
343
+ <div>
344
+ <div className="flex items-center gap-2">
345
+ <h3 className="text-base font-semibold text-gray-900">{data.name || data.id}</h3>
346
+ <span className="text-[11px] text-gray-500 font-medium bg-white px-1.5 py-0.5 rounded border border-gray-200">
347
+ v{data.version}
348
+ </span>
349
+ {/* Show counts when collapsed */}
350
+ {isCollapsed && (services.length > 0 || entities.length > 0) && (
351
+ <span className="text-[11px] text-gray-400 ml-1">
352
+ {services.length > 0 && `${services.length} service${services.length > 1 ? 's' : ''}`}
353
+ {services.length > 0 && entities.length > 0 && ', '}
354
+ {entities.length > 0 && `${entities.length} entit${entities.length > 1 ? 'ies' : 'y'}`}
355
+ </span>
356
+ )}
357
+ </div>
358
+ <span className="text-[11px] text-gray-500 font-medium">Subdomain</span>
359
+ </div>
246
360
  </div>
247
- <div className="flex gap-2">
248
- <button
249
- onClick={() => setIsCollapsed(!isCollapsed)}
250
- className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
251
- >
361
+ <div className="flex items-center gap-1">
362
+ <div className="p-2 text-gray-400">
252
363
  {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
253
- </button>
364
+ </div>
254
365
  <a
255
366
  href={buildUrl(`/architecture/domains/${data.id}/${data.version}`)}
256
- className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
257
- title="Expand domain architecture"
258
367
  onClick={(e) => e.stopPropagation()}
368
+ className="p-2 text-gray-400 hover:text-gray-600 hover:bg-white rounded-lg transition-colors"
369
+ title="View subdomain architecture"
259
370
  >
260
- <ArrowsPointingOutIcon className="h-5 w-5" />
371
+ <ArrowTopRightOnSquareIcon className="h-5 w-5" />
261
372
  </a>
262
373
  </div>
263
374
  </div>
264
375
 
265
376
  {!isCollapsed && (
266
- <>
377
+ <div className="p-5 space-y-5">
267
378
  {/* Subdomain Entities */}
268
379
  {entities.length > 0 && (
269
- <div className="mb-4">
270
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Entities</h4>
380
+ <div>
381
+ <div className="flex items-center gap-2 mb-3">
382
+ <BoxIcon className="h-4 w-4 text-purple-600" />
383
+ <h4 className="text-sm font-semibold text-gray-700">Entities</h4>
384
+ <span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full font-medium">{entities.length}</span>
385
+ </div>
271
386
  <div className="flex flex-wrap gap-2">
272
387
  {entities.map((entity: any) => {
273
388
  const entityId = entity?.data?.id || entity?.id;
@@ -280,11 +395,14 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
280
395
  {/* Subdomain Services */}
281
396
  {services.length > 0 && (
282
397
  <div>
283
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Services</h4>
284
- <div className="grid gap-4 xl:grid-cols-2">
398
+ <div className="flex items-center gap-2 mb-3">
399
+ <ServerIcon className="h-4 w-4 text-pink-600" />
400
+ <h4 className="text-sm font-semibold text-gray-700">Services</h4>
401
+ <span className="text-xs text-gray-500 bg-gray-100 px-2 py-0.5 rounded-full font-medium">{services.length}</span>
402
+ </div>
403
+ <div className="space-y-3">
285
404
  {services.map((service: any) => {
286
405
  const serviceId = service?.data?.id || service?.id;
287
- // Ensure we pass the service down with its messages populated
288
406
  return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
289
407
  })}
290
408
  </div>
@@ -292,9 +410,9 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
292
410
  )}
293
411
 
294
412
  {entities.length === 0 && services.length === 0 && (
295
- <p className="text-sm text-gray-500 italic">No entities or services in this subdomain</p>
413
+ <p className="text-sm text-gray-400 italic text-center py-4">No entities or services in this subdomain</p>
296
414
  )}
297
- </>
415
+ </div>
298
416
  )}
299
417
  </div>
300
418
  );
@@ -306,7 +424,7 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
306
424
 
307
425
  export default function DomainGrid({ domain }: DomainGridProps) {
308
426
  const data = domain?.data;
309
- if (!data) return <div>No domain data</div>;
427
+ if (!data) return <div className="text-gray-500">No domain data</div>;
310
428
 
311
429
  const subdomains = data.domains || [];
312
430
  const entities = data.entities || [];
@@ -334,39 +452,43 @@ export default function DomainGrid({ domain }: DomainGridProps) {
334
452
  );
335
453
 
336
454
  return (
337
- <div className="space-y-6">
338
- {/* Domain Container - Yellow */}
339
- <div className="bg-yellow-50 border-2 border-yellow-400 rounded-xl p-6">
340
- {/* Domain Header */}
341
- <div className="flex items-center justify-between mb-4">
342
- <div className="flex items-center gap-3">
343
- <RectangleGroupIcon className="h-7 w-7 text-yellow-600" />
344
- <h1 className="text-2xl font-bold text-gray-900">{data.name || data.id}</h1>
345
- <span className="px-2 py-0.5 bg-yellow-200 text-yellow-800 text-xs font-medium rounded">v{data.version}</span>
346
- <span className="px-2 py-0.5 bg-yellow-300 text-yellow-900 text-xs font-medium rounded">Domain</span>
455
+ <div className="w-full">
456
+ {/* Domain Header - Doc style */}
457
+ <div className="border-b border-gray-200 md:pb-4">
458
+ <div className="flex items-start justify-between">
459
+ <div>
460
+ <h2 className="text-2xl md:text-4xl font-bold text-black">{data.name || data.id}</h2>
461
+ {data.summary && <p className="text-lg pt-2 text-gray-500 font-light">{data.summary}</p>}
347
462
  </div>
348
- <div className="flex gap-3">
463
+ <div className="flex items-center gap-2 flex-shrink-0">
349
464
  <a
350
465
  href={buildUrl(`/docs/domains/${data.id}/${data.version}`)}
351
- className="text-sm bg-white px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 transition-colors"
466
+ className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all"
352
467
  >
353
468
  View docs
469
+ <ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
354
470
  </a>
355
471
  <a
356
472
  href={buildUrl(`/visualiser/domains/${data.id}/${data.version}`)}
357
- className="text-sm bg-white px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 transition-colors"
473
+ className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-gray-800 rounded-lg hover:bg-gray-900 transition-all"
358
474
  >
359
475
  Visualizer
476
+ <ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
360
477
  </a>
361
478
  </div>
362
479
  </div>
480
+ </div>
363
481
 
364
- {data.summary && <p className="text-gray-600 mb-4">{data.summary}</p>}
365
-
482
+ {/* Domain Content */}
483
+ <div className="py-4 space-y-8">
366
484
  {/* Domain Entities */}
367
485
  {entities.length > 0 && (
368
- <div className="mb-6">
369
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Entities</h3>
486
+ <div>
487
+ <div className="flex items-center gap-2 mb-4">
488
+ <BoxIcon className="h-5 w-5 text-purple-600" />
489
+ <h3 className="text-lg font-semibold text-gray-900">Entities</h3>
490
+ <span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">{entities.length}</span>
491
+ </div>
370
492
  <div className="flex flex-wrap gap-2">
371
493
  {entities.map((entity: any) => {
372
494
  const entityId = entity?.data?.id || entity?.id;
@@ -376,11 +498,17 @@ export default function DomainGrid({ domain }: DomainGridProps) {
376
498
  </div>
377
499
  )}
378
500
 
379
- {/* Top-level Services (not in subdomains) */}
501
+ {/* Top-level Services */}
380
502
  {topLevelServices.length > 0 && (
381
- <div className="mb-6">
382
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Services</h3>
383
- <div className="grid gap-4 xl:grid-cols-2">
503
+ <div>
504
+ <div className="flex items-center gap-2 mb-4">
505
+ <ServerIcon className="h-5 w-5 text-pink-600" />
506
+ <h3 className="text-lg font-semibold text-gray-900">Services</h3>
507
+ <span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">
508
+ {topLevelServices.length}
509
+ </span>
510
+ </div>
511
+ <div className="space-y-3">
384
512
  {topLevelServices.map((service: any) => {
385
513
  const serviceId = service?.data?.id || service?.id;
386
514
  return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
@@ -389,10 +517,16 @@ export default function DomainGrid({ domain }: DomainGridProps) {
389
517
  </div>
390
518
  )}
391
519
 
392
- {/* Subdomains - nested inside domain */}
520
+ {/* Subdomains */}
393
521
  {subdomains.length > 0 && (
394
522
  <div>
395
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Subdomains</h3>
523
+ <div className="flex items-center gap-2 mb-4">
524
+ <RectangleGroupIcon className="h-5 w-5 text-orange-600" />
525
+ <h3 className="text-lg font-semibold text-gray-900">Subdomains</h3>
526
+ <span className="text-sm text-gray-500 bg-gray-100 px-2.5 py-0.5 rounded-full font-medium">
527
+ {subdomains.length}
528
+ </span>
529
+ </div>
396
530
  <div className="space-y-4">
397
531
  {subdomains.map((subdomain: any) => {
398
532
  const subdomainId = subdomain?.data?.id || subdomain?.id;
@@ -404,7 +538,10 @@ export default function DomainGrid({ domain }: DomainGridProps) {
404
538
 
405
539
  {/* Empty state */}
406
540
  {entities.length === 0 && services.length === 0 && subdomains.length === 0 && (
407
- <div className="text-center py-8">
541
+ <div className="text-center py-12">
542
+ <div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-2xl">
543
+ <RectangleGroupIcon className="h-8 w-8 text-gray-400" />
544
+ </div>
408
545
  <p className="text-gray-500">This domain has no entities, services, or subdomains defined.</p>
409
546
  </div>
410
547
  )}