@eventcatalog/core 3.0.0-beta.26 → 3.0.0-beta.28

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.
@@ -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
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,175 +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
- <span className="text-xs font-semibold text-blue-700 uppercase">Inbound Messages</span>
139
- <span className="text-xs text-blue-500">({receives.length})</span>
140
- </div>
141
- {receives.length > 0 ? (
142
- <div className="space-y-1.5">
143
- {receives.slice(0, 4).map((msg: any, idx: number) => {
144
- const msgId = msg?.data?.id || msg?.id;
145
- return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
146
- })}
147
- {receives.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{receives.length - 4} more</p>}
148
- </div>
149
- ) : (
150
- <p className="text-[10px] text-gray-400 italic">No incoming messages</p>
151
- )}
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" />
152
272
  </div>
153
-
154
- {/* Service Icon (Center) */}
155
- <div className="flex items-center">
156
- <div className="bg-pink-100 border-2 border-pink-300 rounded-lg p-3">
157
- <ServerIcon className="h-6 w-6 text-pink-500" />
158
- </div>
159
- </div>
160
-
161
- {/* Sends (Outbound) */}
162
- <div className="flex-1 bg-green-50 border border-green-200 rounded-lg p-3">
163
- <div className="flex items-center gap-1.5 mb-2">
164
- <span className="text-xs font-semibold text-green-700 uppercase">Outbound Messages</span>
165
- <span className="text-xs text-green-500">({sends.length})</span>
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>
166
277
  </div>
167
- {sends.length > 0 ? (
168
- <div className="space-y-1.5">
169
- {sends.slice(0, 4).map((msg: any, idx: number) => {
170
- const msgId = msg?.data?.id || msg?.id;
171
- return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
172
- })}
173
- {sends.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{sends.length - 4} more</p>}
174
- </div>
175
- ) : (
176
- <p className="text-[10px] text-gray-400 italic">No outgoing messages</p>
177
- )}
278
+ {data.summary && <p className="text-xs text-gray-500 line-clamp-1 mt-0.5 max-w-md">{data.summary}</p>}
178
279
  </div>
179
280
  </div>
180
- )}
181
-
182
- {/* Container Relationships */}
183
- {hasContainers && (
184
- <div className="mt-4 pt-4 border-t border-gray-200 grid grid-cols-2 gap-4">
185
- {/* Reads From */}
186
- {readsFrom.length > 0 && (
187
- <div>
188
- <div className="flex items-center gap-1.5 mb-2">
189
- <CircleStackIcon className="h-3.5 w-3.5 text-orange-500" />
190
- <span className="text-xs font-semibold text-gray-700">Reads from</span>
191
- </div>
192
- <div className="flex flex-wrap gap-1.5">
193
- {readsFrom.slice(0, 3).map((container: any, idx: number) => {
194
- const containerId = container?.data?.id || container?.id;
195
- return containerId ? <ContainerBadge key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
196
- })}
197
- {readsFrom.length > 3 && <span className="text-[10px] text-gray-500">+{readsFrom.length - 3} more</span>}
198
- </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
+ ))}
199
297
  </div>
200
298
  )}
201
-
202
- {/* Writes To */}
203
- {writesTo.length > 0 && (
204
- <div>
205
- <div className="flex items-center gap-1.5 mb-2">
206
- <CircleStackIcon className="h-3.5 w-3.5 text-purple-500" />
207
- <span className="text-xs font-semibold text-gray-700">Writes to</span>
208
- </div>
209
- <div className="flex flex-wrap gap-1.5">
210
- {writesTo.slice(0, 3).map((container: any, idx: number) => {
211
- const containerId = container?.data?.id || container?.id;
212
- return containerId ? (
213
- <ContainerBadge key={`${containerId}-${idx}`} container={container} type="writes" />
214
- ) : null;
215
- })}
216
- {writesTo.length > 3 && <span className="text-[10px] text-gray-500">+{writesTo.length - 3} more</span>}
217
- </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" />}
218
302
  </div>
219
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>
220
312
  </div>
313
+ </div>
314
+
315
+ {/* Expanded Content - Compact Flow */}
316
+ {!isCollapsed && hasContent && (
317
+ <ServiceExpandedContent receives={receives} sends={sends} readsFrom={readsFrom} writesTo={writesTo} />
221
318
  )}
222
319
  </div>
223
320
  );
@@ -225,7 +322,7 @@ const ServiceCard = memo(({ service }: { service: any }) => {
225
322
 
226
323
  const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
227
324
  const data = subdomain?.data || subdomain;
228
- const [isCollapsed, setIsCollapsed] = useState(false);
325
+ const [isCollapsed, setIsCollapsed] = useState(true);
229
326
 
230
327
  if (!data?.id) return null;
231
328
 
@@ -233,39 +330,59 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
233
330
  const entities = data.entities || [];
234
331
 
235
332
  return (
236
- <div className="bg-orange-50 border-2 border-orange-400 rounded-lg p-6">
237
- {/* Subdomain Header */}
238
- <div className="flex items-center justify-between mb-4">
239
- <div className="flex items-center gap-2">
240
- <RectangleGroupIcon className="h-5 w-5 text-orange-500" />
241
- <h3 className="text-lg font-semibold text-gray-900">{data.name || data.id}</h3>
242
- <span className="text-xs text-gray-500">v{data.version}</span>
243
- <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>
244
360
  </div>
245
- <div className="flex gap-2">
246
- <button
247
- onClick={() => setIsCollapsed(!isCollapsed)}
248
- className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
249
- >
361
+ <div className="flex items-center gap-1">
362
+ <div className="p-2 text-gray-400">
250
363
  {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
251
- </button>
364
+ </div>
252
365
  <a
253
366
  href={buildUrl(`/architecture/domains/${data.id}/${data.version}`)}
254
- className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
255
- title="Expand domain architecture"
256
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"
257
370
  >
258
- <ArrowsPointingOutIcon className="h-5 w-5" />
371
+ <ArrowTopRightOnSquareIcon className="h-5 w-5" />
259
372
  </a>
260
373
  </div>
261
374
  </div>
262
375
 
263
376
  {!isCollapsed && (
264
- <>
377
+ <div className="p-5 space-y-5">
265
378
  {/* Subdomain Entities */}
266
379
  {entities.length > 0 && (
267
- <div className="mb-4">
268
- <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>
269
386
  <div className="flex flex-wrap gap-2">
270
387
  {entities.map((entity: any) => {
271
388
  const entityId = entity?.data?.id || entity?.id;
@@ -278,11 +395,14 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
278
395
  {/* Subdomain Services */}
279
396
  {services.length > 0 && (
280
397
  <div>
281
- <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Services</h4>
282
- <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">
283
404
  {services.map((service: any) => {
284
405
  const serviceId = service?.data?.id || service?.id;
285
- // Ensure we pass the service down with its messages populated
286
406
  return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
287
407
  })}
288
408
  </div>
@@ -290,9 +410,9 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
290
410
  )}
291
411
 
292
412
  {entities.length === 0 && services.length === 0 && (
293
- <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>
294
414
  )}
295
- </>
415
+ </div>
296
416
  )}
297
417
  </div>
298
418
  );
@@ -304,7 +424,7 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
304
424
 
305
425
  export default function DomainGrid({ domain }: DomainGridProps) {
306
426
  const data = domain?.data;
307
- if (!data) return <div>No domain data</div>;
427
+ if (!data) return <div className="text-gray-500">No domain data</div>;
308
428
 
309
429
  const subdomains = data.domains || [];
310
430
  const entities = data.entities || [];
@@ -332,39 +452,43 @@ export default function DomainGrid({ domain }: DomainGridProps) {
332
452
  );
333
453
 
334
454
  return (
335
- <div className="space-y-6">
336
- {/* Domain Container - Yellow */}
337
- <div className="bg-yellow-50 border-2 border-yellow-400 rounded-xl p-6">
338
- {/* Domain Header */}
339
- <div className="flex items-center justify-between mb-4">
340
- <div className="flex items-center gap-3">
341
- <RectangleGroupIcon className="h-7 w-7 text-yellow-600" />
342
- <h1 className="text-2xl font-bold text-gray-900">{data.name || data.id}</h1>
343
- <span className="px-2 py-0.5 bg-yellow-200 text-yellow-800 text-xs font-medium rounded">v{data.version}</span>
344
- <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>}
345
462
  </div>
346
- <div className="flex gap-3">
463
+ <div className="flex items-center gap-2 flex-shrink-0">
347
464
  <a
348
465
  href={buildUrl(`/docs/domains/${data.id}/${data.version}`)}
349
- 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"
350
467
  >
351
468
  View docs
469
+ <ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
352
470
  </a>
353
471
  <a
354
472
  href={buildUrl(`/visualiser/domains/${data.id}/${data.version}`)}
355
- 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"
356
474
  >
357
475
  Visualizer
476
+ <ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-400" />
358
477
  </a>
359
478
  </div>
360
479
  </div>
480
+ </div>
361
481
 
362
- {data.summary && <p className="text-gray-600 mb-4">{data.summary}</p>}
363
-
482
+ {/* Domain Content */}
483
+ <div className="py-4 space-y-8">
364
484
  {/* Domain Entities */}
365
485
  {entities.length > 0 && (
366
- <div className="mb-6">
367
- <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>
368
492
  <div className="flex flex-wrap gap-2">
369
493
  {entities.map((entity: any) => {
370
494
  const entityId = entity?.data?.id || entity?.id;
@@ -374,11 +498,17 @@ export default function DomainGrid({ domain }: DomainGridProps) {
374
498
  </div>
375
499
  )}
376
500
 
377
- {/* Top-level Services (not in subdomains) */}
501
+ {/* Top-level Services */}
378
502
  {topLevelServices.length > 0 && (
379
- <div className="mb-6">
380
- <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Services</h3>
381
- <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">
382
512
  {topLevelServices.map((service: any) => {
383
513
  const serviceId = service?.data?.id || service?.id;
384
514
  return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
@@ -387,10 +517,16 @@ export default function DomainGrid({ domain }: DomainGridProps) {
387
517
  </div>
388
518
  )}
389
519
 
390
- {/* Subdomains - nested inside domain */}
520
+ {/* Subdomains */}
391
521
  {subdomains.length > 0 && (
392
522
  <div>
393
- <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>
394
530
  <div className="space-y-4">
395
531
  {subdomains.map((subdomain: any) => {
396
532
  const subdomainId = subdomain?.data?.id || subdomain?.id;
@@ -402,7 +538,10 @@ export default function DomainGrid({ domain }: DomainGridProps) {
402
538
 
403
539
  {/* Empty state */}
404
540
  {entities.length === 0 && services.length === 0 && subdomains.length === 0 && (
405
- <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>
406
545
  <p className="text-gray-500">This domain has no entities, services, or subdomains defined.</p>
407
546
  </div>
408
547
  )}