@eventcatalog/visualiser 3.19.0 → 3.20.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.
package/dist/index.js CHANGED
@@ -104,7 +104,7 @@ module.exports = __toCommonJS(index_exports);
104
104
  var import_react79 = require("react");
105
105
  var import_react_dom = require("react-dom");
106
106
  var import_react80 = require("@xyflow/react");
107
- var import_lucide_react31 = require("lucide-react");
107
+ var import_lucide_react32 = require("lucide-react");
108
108
  var DropdownMenu2 = __toESM(require("@radix-ui/react-dropdown-menu"));
109
109
  var import_html_to_image = require("html-to-image");
110
110
 
@@ -6101,7 +6101,64 @@ var FlowEdge_default = (0, import_react59.memo)(function CustomEdge({
6101
6101
 
6102
6102
  // src/components/VisualiserSearch.tsx
6103
6103
  var import_react61 = require("react");
6104
+ var import_lucide_react25 = require("lucide-react");
6104
6105
  var import_jsx_runtime32 = require("react/jsx-runtime");
6106
+ var formatVersionedName = (name, version) => {
6107
+ if (version) {
6108
+ const nameWithoutVersion = name.replace(
6109
+ new RegExp(`-v?${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
6110
+ ""
6111
+ );
6112
+ return `${nameWithoutVersion} (v${version})`;
6113
+ }
6114
+ const versionMatch = name.match(
6115
+ /^(.+)-v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/
6116
+ );
6117
+ if (!versionMatch) return name;
6118
+ return `${versionMatch[1]} (v${versionMatch[2]})`;
6119
+ };
6120
+ var normalizeCollectionType = (type) => {
6121
+ const aliases = {
6122
+ event: "events",
6123
+ command: "commands",
6124
+ query: "queries",
6125
+ service: "services",
6126
+ domain: "domains",
6127
+ channel: "channels",
6128
+ entity: "entities"
6129
+ };
6130
+ return aliases[type] || type;
6131
+ };
6132
+ var splitVersionedName = (name, version) => {
6133
+ if (version) {
6134
+ return {
6135
+ id: name.replace(
6136
+ new RegExp(`-v?${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
6137
+ ""
6138
+ ),
6139
+ version
6140
+ };
6141
+ }
6142
+ const versionMatch = name.match(
6143
+ /^(.+)-v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/
6144
+ );
6145
+ if (!versionMatch) return { id: name, version: void 0 };
6146
+ return { id: versionMatch[1], version: versionMatch[2] };
6147
+ };
6148
+ var getResourceKey = ({
6149
+ type,
6150
+ id,
6151
+ name,
6152
+ version
6153
+ }) => {
6154
+ const parsed = splitVersionedName(id || name, version);
6155
+ return `${normalizeCollectionType(type)}:${parsed.id}:${parsed.version || ""}`.toLowerCase();
6156
+ };
6157
+ var getNodeResourceData = (data, key) => {
6158
+ const resource = data?.[key];
6159
+ if (!resource || typeof resource !== "object") return void 0;
6160
+ return "data" in resource && resource.data ? resource.data : resource;
6161
+ };
6105
6162
  var VisualiserSearch = (0, import_react61.memo)(
6106
6163
  (0, import_react61.forwardRef)(
6107
6164
  ({ nodes, onNodeSelect, onClear, onPaneClick: _onPaneClick }, ref) => {
@@ -6111,6 +6168,8 @@ var VisualiserSearch = (0, import_react61.memo)(
6111
6168
  const [selectedSuggestionIndex, setSelectedSuggestionIndex] = (0, import_react61.useState)(-1);
6112
6169
  const searchInputRef = (0, import_react61.useRef)(null);
6113
6170
  const containerRef = (0, import_react61.useRef)(null);
6171
+ const suggestionsListRef = (0, import_react61.useRef)(null);
6172
+ const suggestionItemRefs = (0, import_react61.useRef)([]);
6114
6173
  const hideSuggestions = (0, import_react61.useCallback)(() => {
6115
6174
  setShowSuggestions(false);
6116
6175
  setSelectedSuggestionIndex(-1);
@@ -6129,84 +6188,277 @@ var VisualiserSearch = (0, import_react61.memo)(
6129
6188
  if (node.type === "messageGroupExpanded") {
6130
6189
  return node.data?.groupName || node.id;
6131
6190
  }
6132
- const name = node.data?.message?.data?.name || node.data?.service?.data?.name || node.data?.domain?.data?.name || node.data?.entity?.data?.name || node.data?.name || node.id;
6133
- const version = node.data?.message?.data?.version || node.data?.service?.data?.version || node.data?.domain?.data?.version || node.data?.entity?.data?.version || node.data?.version;
6134
- return version ? `${name} (v${version})` : name;
6191
+ const message = getNodeResourceData(node.data, "message");
6192
+ const service = getNodeResourceData(node.data, "service");
6193
+ const domain = getNodeResourceData(node.data, "domain");
6194
+ const entity = getNodeResourceData(node.data, "entity");
6195
+ const channel = getNodeResourceData(node.data, "channel");
6196
+ const dataProduct = getNodeResourceData(node.data, "dataProduct");
6197
+ const data = getNodeResourceData(node.data, "data");
6198
+ const name = message?.name || message?.id || service?.name || service?.id || domain?.name || domain?.id || entity?.name || entity?.id || channel?.name || channel?.id || dataProduct?.name || dataProduct?.id || data?.name || data?.id || node.data?.name || node.id;
6199
+ const version = message?.version || service?.version || domain?.version || entity?.version || channel?.version || dataProduct?.version || data?.version || node.data?.version;
6200
+ return formatVersionedName(name, version);
6135
6201
  }, []);
6136
- const getNodeTypeColorClass = (0, import_react61.useCallback)((nodeType) => {
6137
- const colorClasses = {
6138
- events: "bg-orange-600 text-white",
6139
- services: "bg-pink-600 text-white",
6140
- flows: "bg-teal-600 text-white",
6141
- commands: "bg-blue-600 text-white",
6142
- queries: "bg-green-600 text-white",
6143
- channels: "bg-gray-600 text-white",
6144
- domains: "bg-yellow-500 text-white",
6145
- externalSystem: "bg-pink-600 text-white",
6146
- actor: "bg-yellow-500 text-white",
6147
- step: "bg-gray-700 text-white",
6148
- user: "bg-yellow-500 text-white",
6149
- custom: "bg-gray-500 text-white",
6150
- field: "bg-cyan-600 text-white",
6151
- messageGroup: "bg-violet-600 text-white",
6152
- messageGroupExpanded: "bg-violet-600 text-white"
6202
+ const getNodeResourceKey = (0, import_react61.useCallback)(
6203
+ (node, label) => {
6204
+ if (node.type === "messageGroup" || node.type === "messageGroupExpanded") {
6205
+ return `${node.type}:${node.id}`.toLowerCase();
6206
+ }
6207
+ const data = node.data;
6208
+ const resource = getNodeResourceData(data, "message") || getNodeResourceData(data, "service") || getNodeResourceData(data, "domain") || getNodeResourceData(data, "entity") || getNodeResourceData(data, "channel") || getNodeResourceData(data, "dataProduct") || data?.data || data;
6209
+ return getResourceKey({
6210
+ type: node.type || "unknown",
6211
+ id: resource?.id,
6212
+ name: resource?.name || resource?.id || label || node.id,
6213
+ version: resource?.version || data?.version
6214
+ });
6215
+ },
6216
+ []
6217
+ );
6218
+ const dedupeSearchSuggestions = (0, import_react61.useCallback)(
6219
+ (suggestions) => {
6220
+ const uniqueSuggestions = [];
6221
+ const indexByResourceKey = /* @__PURE__ */ new Map();
6222
+ suggestions.forEach((suggestion) => {
6223
+ const existingIndex = indexByResourceKey.get(
6224
+ suggestion.resourceKey
6225
+ );
6226
+ if (existingIndex === void 0) {
6227
+ indexByResourceKey.set(
6228
+ suggestion.resourceKey,
6229
+ uniqueSuggestions.length
6230
+ );
6231
+ uniqueSuggestions.push(suggestion);
6232
+ return;
6233
+ }
6234
+ if (suggestion.isGroupedMessage && !uniqueSuggestions[existingIndex].isGroupedMessage) {
6235
+ uniqueSuggestions[existingIndex] = suggestion;
6236
+ }
6237
+ });
6238
+ return uniqueSuggestions;
6239
+ },
6240
+ []
6241
+ );
6242
+ const getSearchSuggestions = (0, import_react61.useCallback)(
6243
+ (nodesToIndex) => {
6244
+ const suggestions = nodesToIndex.flatMap((node) => {
6245
+ const nodeName = getNodeDisplayName(node);
6246
+ const suggestions2 = [
6247
+ {
6248
+ key: node.id,
6249
+ node,
6250
+ label: nodeName,
6251
+ searchText: nodeName,
6252
+ type: node.type || "unknown",
6253
+ resourceKey: getNodeResourceKey(node, nodeName)
6254
+ }
6255
+ ];
6256
+ if (node.type !== "messageGroup") return suggestions2;
6257
+ const groupName = node.data?.groupName || nodeName;
6258
+ const groupedMessages = node.data?.messages || [];
6259
+ groupedMessages.forEach((item, index) => {
6260
+ const message = item.message;
6261
+ const messageName = message?.data?.name || message?.data?.id;
6262
+ if (!messageName) return;
6263
+ const version = message?.data?.version;
6264
+ const label = formatVersionedName(messageName, version);
6265
+ suggestions2.push({
6266
+ key: `${node.id}:${message?.data?.id || messageName}:${version || index}`,
6267
+ node,
6268
+ label,
6269
+ searchText: `${label} ${groupName}`,
6270
+ type: message?.collection || "message",
6271
+ resourceKey: getResourceKey({
6272
+ type: message?.collection || "message",
6273
+ id: message?.data?.id,
6274
+ name: messageName,
6275
+ version
6276
+ }),
6277
+ groupName,
6278
+ isGroupedMessage: true
6279
+ });
6280
+ });
6281
+ return suggestions2;
6282
+ });
6283
+ return dedupeSearchSuggestions(suggestions);
6284
+ },
6285
+ [dedupeSearchSuggestions, getNodeDisplayName, getNodeResourceKey]
6286
+ );
6287
+ const getNodeTypeMeta = (0, import_react61.useCallback)((nodeType) => {
6288
+ const meta = {
6289
+ events: {
6290
+ label: "Event",
6291
+ Icon: import_lucide_react25.Zap,
6292
+ iconClass: "border-orange-500/25 bg-orange-500/10 text-orange-500",
6293
+ badgeClass: "border-orange-500/25 bg-orange-500/10 text-orange-700 dark:text-orange-300"
6294
+ },
6295
+ event: {
6296
+ label: "Event",
6297
+ Icon: import_lucide_react25.Zap,
6298
+ iconClass: "border-orange-500/25 bg-orange-500/10 text-orange-500",
6299
+ badgeClass: "border-orange-500/25 bg-orange-500/10 text-orange-700 dark:text-orange-300"
6300
+ },
6301
+ commands: {
6302
+ label: "Command",
6303
+ Icon: import_lucide_react25.MessageSquare,
6304
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6305
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6306
+ },
6307
+ command: {
6308
+ label: "Command",
6309
+ Icon: import_lucide_react25.MessageSquare,
6310
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6311
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6312
+ },
6313
+ queries: {
6314
+ label: "Query",
6315
+ Icon: import_lucide_react25.Search,
6316
+ iconClass: "border-green-500/25 bg-green-500/10 text-green-500",
6317
+ badgeClass: "border-green-500/25 bg-green-500/10 text-green-700 dark:text-green-300"
6318
+ },
6319
+ query: {
6320
+ label: "Query",
6321
+ Icon: import_lucide_react25.Search,
6322
+ iconClass: "border-green-500/25 bg-green-500/10 text-green-500",
6323
+ badgeClass: "border-green-500/25 bg-green-500/10 text-green-700 dark:text-green-300"
6324
+ },
6325
+ services: {
6326
+ label: "Service",
6327
+ Icon: import_lucide_react25.Server,
6328
+ iconClass: "border-pink-500/25 bg-pink-500/10 text-pink-500",
6329
+ badgeClass: "border-pink-500/25 bg-pink-500/10 text-pink-700 dark:text-pink-300"
6330
+ },
6331
+ domains: {
6332
+ label: "Domain",
6333
+ Icon: import_lucide_react25.Blocks,
6334
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6335
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6336
+ },
6337
+ flows: {
6338
+ label: "Flow",
6339
+ Icon: import_lucide_react25.Workflow,
6340
+ iconClass: "border-teal-500/25 bg-teal-500/10 text-teal-500",
6341
+ badgeClass: "border-teal-500/25 bg-teal-500/10 text-teal-700 dark:text-teal-300"
6342
+ },
6343
+ channels: {
6344
+ label: "Channel",
6345
+ Icon: import_lucide_react25.ListTree,
6346
+ iconClass: "border-gray-500/25 bg-gray-500/10 text-gray-500",
6347
+ badgeClass: "border-gray-500/25 bg-gray-500/10 text-gray-700 dark:text-gray-300"
6348
+ },
6349
+ data: {
6350
+ label: "Data",
6351
+ Icon: import_lucide_react25.Database,
6352
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6353
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6354
+ },
6355
+ entities: {
6356
+ label: "Entity",
6357
+ Icon: import_lucide_react25.Database,
6358
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6359
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6360
+ },
6361
+ externalSystem: {
6362
+ label: "External",
6363
+ Icon: import_lucide_react25.Server,
6364
+ iconClass: "border-pink-500/25 bg-pink-500/10 text-pink-500",
6365
+ badgeClass: "border-pink-500/25 bg-pink-500/10 text-pink-700 dark:text-pink-300"
6366
+ },
6367
+ actor: {
6368
+ label: "Actor",
6369
+ Icon: import_lucide_react25.User,
6370
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6371
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6372
+ },
6373
+ user: {
6374
+ label: "User",
6375
+ Icon: import_lucide_react25.User,
6376
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6377
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6378
+ },
6379
+ messageGroup: {
6380
+ label: "Group",
6381
+ Icon: import_lucide_react25.Layers,
6382
+ iconClass: "border-violet-500/25 bg-violet-500/10 text-violet-500",
6383
+ badgeClass: "border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300"
6384
+ },
6385
+ messageGroupExpanded: {
6386
+ label: "Group",
6387
+ Icon: import_lucide_react25.Layers,
6388
+ iconClass: "border-violet-500/25 bg-violet-500/10 text-violet-500",
6389
+ badgeClass: "border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300"
6390
+ }
6391
+ };
6392
+ return meta[nodeType] || {
6393
+ label: nodeType,
6394
+ Icon: import_lucide_react25.Layers,
6395
+ iconClass: "border-gray-500/25 bg-gray-500/10 text-gray-500",
6396
+ badgeClass: "border-gray-500/25 bg-gray-500/10 text-gray-700 dark:text-gray-300"
6153
6397
  };
6154
- return colorClasses[nodeType] || "bg-gray-100 text-gray-700";
6155
6398
  }, []);
6156
6399
  const handleSearchChange = (0, import_react61.useCallback)(
6157
6400
  (event) => {
6158
6401
  const query = event.target.value;
6159
6402
  setSearchQuery(query);
6160
6403
  if (query.length > 0) {
6161
- const filtered = nodes.filter((node) => {
6162
- const nodeName = getNodeDisplayName(node);
6163
- return nodeName.toLowerCase().includes(query.toLowerCase());
6164
- });
6404
+ const search = query.toLowerCase();
6405
+ const filtered = getSearchSuggestions(nodes).filter(
6406
+ (suggestion) => suggestion.searchText.toLowerCase().includes(search)
6407
+ );
6165
6408
  setFilteredSuggestions(filtered);
6166
6409
  setShowSuggestions(true);
6167
6410
  setSelectedSuggestionIndex(-1);
6168
6411
  } else {
6169
- setFilteredSuggestions(nodes);
6412
+ setFilteredSuggestions(getSearchSuggestions(nodes));
6170
6413
  setShowSuggestions(true);
6171
6414
  setSelectedSuggestionIndex(-1);
6172
6415
  }
6173
6416
  },
6174
- [nodes, getNodeDisplayName]
6417
+ [nodes, getSearchSuggestions]
6175
6418
  );
6176
6419
  const handleSearchFocus = (0, import_react61.useCallback)(() => {
6177
- if (searchQuery.length === 0) {
6178
- setFilteredSuggestions(nodes);
6179
- }
6420
+ const suggestions = getSearchSuggestions(nodes);
6421
+ const search = searchQuery.toLowerCase();
6422
+ setFilteredSuggestions(
6423
+ searchQuery.length === 0 ? suggestions : suggestions.filter(
6424
+ (suggestion) => suggestion.searchText.toLowerCase().includes(search)
6425
+ )
6426
+ );
6180
6427
  setShowSuggestions(true);
6181
6428
  setSelectedSuggestionIndex(-1);
6182
- }, [nodes, searchQuery]);
6429
+ }, [nodes, searchQuery, getSearchSuggestions]);
6183
6430
  const handleSuggestionClick = (0, import_react61.useCallback)(
6184
- (node) => {
6185
- setSearchQuery(getNodeDisplayName(node));
6431
+ (suggestion) => {
6432
+ setSearchQuery("");
6433
+ setFilteredSuggestions([]);
6186
6434
  setShowSuggestions(false);
6187
- onNodeSelect(node);
6435
+ setSelectedSuggestionIndex(-1);
6436
+ onNodeSelect(suggestion.node);
6188
6437
  },
6189
- [onNodeSelect, getNodeDisplayName]
6438
+ [onNodeSelect]
6190
6439
  );
6191
6440
  const handleSearchKeyDown = (0, import_react61.useCallback)(
6192
6441
  (event) => {
6193
- if (!showSuggestions || filteredSuggestions.length === 0) return;
6194
6442
  switch (event.key) {
6195
6443
  case "ArrowDown":
6196
6444
  event.preventDefault();
6445
+ if (filteredSuggestions.length === 0) return;
6446
+ setShowSuggestions(true);
6197
6447
  setSelectedSuggestionIndex(
6198
6448
  (prev) => prev < filteredSuggestions.length - 1 ? prev + 1 : 0
6199
6449
  );
6200
6450
  break;
6201
6451
  case "ArrowUp":
6202
6452
  event.preventDefault();
6453
+ if (filteredSuggestions.length === 0) return;
6454
+ setShowSuggestions(true);
6203
6455
  setSelectedSuggestionIndex(
6204
6456
  (prev) => prev > 0 ? prev - 1 : filteredSuggestions.length - 1
6205
6457
  );
6206
6458
  break;
6207
6459
  case "Enter":
6208
6460
  event.preventDefault();
6209
- if (selectedSuggestionIndex >= 0) {
6461
+ if (showSuggestions && selectedSuggestionIndex >= 0 && selectedSuggestionIndex < filteredSuggestions.length) {
6210
6462
  handleSuggestionClick(
6211
6463
  filteredSuggestions[selectedSuggestionIndex]
6212
6464
  );
@@ -6235,6 +6487,31 @@ var VisualiserSearch = (0, import_react61.memo)(
6235
6487
  searchInputRef.current.focus();
6236
6488
  }
6237
6489
  }, [onClear]);
6490
+ (0, import_react61.useEffect)(() => {
6491
+ suggestionItemRefs.current = suggestionItemRefs.current.slice(
6492
+ 0,
6493
+ filteredSuggestions.length
6494
+ );
6495
+ }, [filteredSuggestions.length]);
6496
+ (0, import_react61.useEffect)(() => {
6497
+ if (!showSuggestions || selectedSuggestionIndex < 0) return;
6498
+ const list = suggestionsListRef.current;
6499
+ const item = suggestionItemRefs.current[selectedSuggestionIndex];
6500
+ if (!list || !item) return;
6501
+ const itemTop = item.offsetTop;
6502
+ const itemBottom = itemTop + item.offsetHeight;
6503
+ const visibleTop = list.scrollTop;
6504
+ const visibleBottom = visibleTop + list.clientHeight;
6505
+ if (itemTop < visibleTop) {
6506
+ list.scrollTop = itemTop;
6507
+ } else if (itemBottom > visibleBottom) {
6508
+ list.scrollTop = itemBottom - list.clientHeight;
6509
+ }
6510
+ }, [
6511
+ showSuggestions,
6512
+ selectedSuggestionIndex,
6513
+ filteredSuggestions.length
6514
+ ]);
6238
6515
  (0, import_react61.useEffect)(() => {
6239
6516
  const handleClickOutside = (event) => {
6240
6517
  if (containerRef.current && !containerRef.current.contains(event.target)) {
@@ -6289,28 +6566,53 @@ var VisualiserSearch = (0, import_react61.memo)(
6289
6566
  }
6290
6567
  )
6291
6568
  ] }),
6292
- showSuggestions && filteredSuggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("div", { className: "absolute top-full left-0 right-0 mt-1 bg-[rgb(var(--ec-card-bg))] border border-[rgb(var(--ec-page-border))] rounded-md shadow-lg z-50 max-h-60 overflow-y-auto", children: filteredSuggestions.map((node, index) => {
6293
- const nodeName = getNodeDisplayName(node);
6294
- const nodeType = node.type || "unknown";
6295
- return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
6296
- "div",
6297
- {
6298
- onClick: () => handleSuggestionClick(node),
6299
- className: `px-4 py-2 cursor-pointer flex items-center justify-between hover:bg-[rgb(var(--ec-page-border)/0.5)] ${index === selectedSuggestionIndex ? "bg-[rgb(var(--ec-accent-subtle))]" : ""}`,
6300
- children: [
6301
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "text-sm font-medium text-[rgb(var(--ec-page-text))]", children: nodeName }),
6302
- /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
6303
- "span",
6304
- {
6305
- className: `text-xs capitalize px-2 py-1 rounded ${getNodeTypeColorClass(nodeType)}`,
6306
- children: nodeType
6307
- }
6308
- )
6309
- ]
6310
- },
6311
- node.id
6312
- );
6313
- }) })
6569
+ showSuggestions && filteredSuggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
6570
+ "div",
6571
+ {
6572
+ ref: suggestionsListRef,
6573
+ className: "absolute top-full left-0 right-0 mt-1 bg-[rgb(var(--ec-card-bg))] border border-[rgb(var(--ec-page-border))] rounded-md shadow-lg z-50 max-h-60 overflow-y-auto",
6574
+ children: filteredSuggestions.map((suggestion, index) => {
6575
+ const nodeTypeMeta = getNodeTypeMeta(suggestion.type);
6576
+ const Icon = nodeTypeMeta.Icon;
6577
+ const isSelected = index === selectedSuggestionIndex;
6578
+ return /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)(
6579
+ "div",
6580
+ {
6581
+ ref: (element) => {
6582
+ suggestionItemRefs.current[index] = element;
6583
+ },
6584
+ onClick: () => handleSuggestionClick(suggestion),
6585
+ onMouseEnter: () => setSelectedSuggestionIndex(index),
6586
+ className: `px-3 py-2 cursor-pointer flex items-start gap-3 ${isSelected ? "bg-[rgb(var(--ec-accent-subtle))] outline outline-1 -outline-offset-1 outline-[rgb(var(--ec-accent))]" : "hover:bg-[rgb(var(--ec-page-border)/0.5)]"}`,
6587
+ children: [
6588
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
6589
+ "span",
6590
+ {
6591
+ className: `mt-0.5 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border ${nodeTypeMeta.iconClass}`,
6592
+ children: /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(Icon, { className: "h-3.5 w-3.5" })
6593
+ }
6594
+ ),
6595
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "min-w-0 flex-1", children: [
6596
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)("span", { className: "block truncate text-sm font-medium text-[rgb(var(--ec-page-text))]", children: suggestion.label }),
6597
+ suggestion.isGroupedMessage && suggestion.groupName && /* @__PURE__ */ (0, import_jsx_runtime32.jsxs)("span", { className: "mt-0.5 block truncate text-xs text-[rgb(var(--ec-page-text-muted))]", children: [
6598
+ "in ",
6599
+ suggestion.groupName
6600
+ ] })
6601
+ ] }),
6602
+ /* @__PURE__ */ (0, import_jsx_runtime32.jsx)(
6603
+ "span",
6604
+ {
6605
+ className: `mt-0.5 flex-shrink-0 rounded border px-2 py-0.5 text-xs font-medium ${nodeTypeMeta.badgeClass}`,
6606
+ children: nodeTypeMeta.label
6607
+ }
6608
+ )
6609
+ ]
6610
+ },
6611
+ `${suggestion.key}:${index}`
6612
+ );
6613
+ })
6614
+ }
6615
+ )
6314
6616
  ] });
6315
6617
  }
6316
6618
  )
@@ -6565,7 +6867,7 @@ var StepWalkthrough_default = (0, import_react62.memo)(function StepWalkthrough(
6565
6867
  // src/components/StudioModal.tsx
6566
6868
  var import_react63 = require("react");
6567
6869
  var Dialog2 = __toESM(require("@radix-ui/react-dialog"));
6568
- var import_lucide_react25 = require("lucide-react");
6870
+ var import_lucide_react26 = require("lucide-react");
6569
6871
  var import_react64 = require("@xyflow/react");
6570
6872
 
6571
6873
  // src/utils/export-node-graph.ts
@@ -6690,10 +6992,10 @@ var StudioModal = ({ isOpen, onClose }) => {
6690
6992
  onClick: handleCopyToClipboard,
6691
6993
  className: `w-full flex items-center justify-center space-x-2 px-4 py-2 text-sm font-medium rounded-md border transition-colors ${copySuccess ? "bg-green-50 border-green-200 text-green-700" : "bg-white border-gray-300 text-gray-700 hover:bg-gray-50"}`,
6692
6994
  children: copySuccess ? /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
6693
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react25.CheckIcon, { className: "w-4 h-4" }),
6995
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react26.CheckIcon, { className: "w-4 h-4" }),
6694
6996
  /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { children: "Copied!" })
6695
6997
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime34.jsxs)(import_jsx_runtime34.Fragment, { children: [
6696
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react25.ClipboardIcon, { className: "w-4 h-4" }),
6998
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react26.ClipboardIcon, { className: "w-4 h-4" }),
6697
6999
  /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { children: "Copy diagram to clipboard" })
6698
7000
  ] })
6699
7001
  }
@@ -6708,7 +7010,7 @@ var StudioModal = ({ isOpen, onClose }) => {
6708
7010
  onClick: handleOpenStudio,
6709
7011
  className: "w-full flex items-center justify-center space-x-2 px-4 py-2 bg-[rgb(var(--ec-accent))] text-white text-sm font-medium rounded-md hover:bg-[rgb(var(--ec-accent-hover))] transition-colors",
6710
7012
  children: [
6711
- /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react25.ExternalLinkIcon, { className: "w-4 h-4" }),
7013
+ /* @__PURE__ */ (0, import_jsx_runtime34.jsx)(import_lucide_react26.ExternalLinkIcon, { className: "w-4 h-4" }),
6712
7014
  /* @__PURE__ */ (0, import_jsx_runtime34.jsx)("span", { children: "Open EventCatalog Studio" })
6713
7015
  ]
6714
7016
  }
@@ -6733,7 +7035,7 @@ var StudioModal_default = StudioModal;
6733
7035
  // src/components/FocusModeModal.tsx
6734
7036
  var import_react69 = require("react");
6735
7037
  var Dialog3 = __toESM(require("@radix-ui/react-dialog"));
6736
- var import_lucide_react27 = require("lucide-react");
7038
+ var import_lucide_react28 = require("lucide-react");
6737
7039
  var import_react70 = require("@xyflow/react");
6738
7040
 
6739
7041
  // src/components/FocusMode/FocusModeContent.tsx
@@ -6847,7 +7149,7 @@ function getNodeDocUrl(node) {
6847
7149
 
6848
7150
  // src/components/FocusMode/FocusModeNodeActions.tsx
6849
7151
  var import_react65 = require("@xyflow/react");
6850
- var import_lucide_react26 = require("lucide-react");
7152
+ var import_lucide_react27 = require("lucide-react");
6851
7153
  var import_jsx_runtime35 = require("react/jsx-runtime");
6852
7154
  var FocusModeNodeActions = ({
6853
7155
  node,
@@ -6894,7 +7196,7 @@ var FocusModeNodeActions = ({
6894
7196
  className: "flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors",
6895
7197
  style: { width: buttonSize, height: buttonSize },
6896
7198
  title: "View documentation",
6897
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react26.FileText, { style: { width: iconSize, height: iconSize } })
7199
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react27.FileText, { style: { width: iconSize, height: iconSize } })
6898
7200
  }
6899
7201
  )
6900
7202
  }
@@ -6922,7 +7224,7 @@ var FocusModeNodeActions = ({
6922
7224
  className: "flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors",
6923
7225
  style: { width: buttonSize, height: buttonSize },
6924
7226
  title: "View documentation",
6925
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react26.FileText, { style: { width: iconSize, height: iconSize } })
7227
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react27.FileText, { style: { width: iconSize, height: iconSize } })
6926
7228
  }
6927
7229
  ),
6928
7230
  /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
@@ -6932,7 +7234,7 @@ var FocusModeNodeActions = ({
6932
7234
  className: "flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors",
6933
7235
  style: { width: buttonSize, height: buttonSize },
6934
7236
  title: "Focus on this node",
6935
- children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react26.ArrowRightLeft, { style: { width: iconSize, height: iconSize } })
7237
+ children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(import_lucide_react27.ArrowRightLeft, { style: { width: iconSize, height: iconSize } })
6936
7238
  }
6937
7239
  )
6938
7240
  ]
@@ -7331,7 +7633,7 @@ var FocusModeModal = ({
7331
7633
  background: isDark ? "rgba(59, 130, 246, 0.18)" : "rgba(59, 130, 246, 0.12)"
7332
7634
  },
7333
7635
  children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(
7334
- import_lucide_react27.FocusIcon,
7636
+ import_lucide_react28.FocusIcon,
7335
7637
  {
7336
7638
  style: {
7337
7639
  width: 20,
@@ -7383,7 +7685,7 @@ var FocusModeModal = ({
7383
7685
  color: isDark ? "#94a3b8" : "#64748b"
7384
7686
  },
7385
7687
  "aria-label": "Close",
7386
- children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react27.XIcon, { style: { width: 20, height: 20 } })
7688
+ children: /* @__PURE__ */ (0, import_jsx_runtime38.jsx)(import_lucide_react28.XIcon, { style: { width: 20, height: 20 } })
7387
7689
  }
7388
7690
  ) })
7389
7691
  ]
@@ -7411,7 +7713,7 @@ var FocusModeModal_default = FocusModeModal;
7411
7713
 
7412
7714
  // src/components/MermaidView.tsx
7413
7715
  var import_react71 = require("react");
7414
- var import_lucide_react28 = require("lucide-react");
7716
+ var import_lucide_react29 = require("lucide-react");
7415
7717
 
7416
7718
  // src/utils/export-mermaid.ts
7417
7719
  var NODE_SHAPE_MAP = {
@@ -7805,7 +8107,7 @@ var MermaidView = ({
7805
8107
  onClick: handleCopyToClipboard,
7806
8108
  className: `p-2.5 rounded-md shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] transition-all duration-150 ${copySuccess ? "bg-green-500 text-white scale-110" : "bg-[rgb(var(--ec-card-bg))] hover:bg-[rgb(var(--ec-page-border))/0.5] text-[rgb(var(--ec-icon-color))] hover:scale-105"}`,
7807
8109
  "aria-label": copySuccess ? "Copied!" : "Copy Mermaid code",
7808
- children: copySuccess ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react28.CheckIcon, { className: "h-5 w-5" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react28.ClipboardIcon, { className: "h-5 w-5" })
8110
+ children: copySuccess ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react29.CheckIcon, { className: "h-5 w-5" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react29.ClipboardIcon, { className: "h-5 w-5" })
7809
8111
  }
7810
8112
  ),
7811
8113
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "absolute top-full right-0 mt-2 px-2 py-1 bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] text-xs rounded shadow-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50", children: copySuccess ? "Copied!" : "Copy Mermaid code" })
@@ -8111,7 +8413,7 @@ var useChannelVisibility = ({
8111
8413
  // src/components/VisualizerDropdownContent.tsx
8112
8414
  var import_react74 = require("react");
8113
8415
  var DropdownMenu = __toESM(require("@radix-ui/react-dropdown-menu"));
8114
- var import_lucide_react29 = require("lucide-react");
8416
+ var import_lucide_react30 = require("lucide-react");
8115
8417
  var import_outline2 = require("@heroicons/react/24/outline");
8116
8418
  var import_jsx_runtime40 = require("react/jsx-runtime");
8117
8419
  var VisualizerDropdownContent = (0, import_react74.memo)(
@@ -8166,7 +8468,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8166
8468
  return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(import_jsx_runtime40.Fragment, { children: [
8167
8469
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(DropdownMenu.Sub, { children: [
8168
8470
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(DropdownMenu.SubTrigger, { className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none", children: [
8169
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Grid3x3, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8471
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Grid3x3, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8170
8472
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Canvas" }),
8171
8473
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8172
8474
  "svg",
@@ -8201,7 +8503,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8201
8503
  onCheckedChange: setIsMermaidView,
8202
8504
  className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2",
8203
8505
  children: [
8204
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Code, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8506
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Code, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8205
8507
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Render as mermaid" }),
8206
8508
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8207
8509
  "div",
@@ -8226,7 +8528,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8226
8528
  onCheckedChange: toggleAnimateMessages,
8227
8529
  className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2",
8228
8530
  children: [
8229
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Zap, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8531
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Zap, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8230
8532
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Simulate Messages" }),
8231
8533
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8232
8534
  "div",
@@ -8250,7 +8552,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8250
8552
  onCheckedChange: toggleChannelsVisibility,
8251
8553
  className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2",
8252
8554
  children: [
8253
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.EyeOff, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8555
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.EyeOff, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8254
8556
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Hide channels" }),
8255
8557
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8256
8558
  "div",
@@ -8274,7 +8576,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8274
8576
  onCheckedChange: setShowMinimap,
8275
8577
  className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2",
8276
8578
  children: [
8277
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Map, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8579
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Map, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8278
8580
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Show minimap" }),
8279
8581
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
8280
8582
  "div",
@@ -8298,7 +8600,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8298
8600
  onClick: handleFitView,
8299
8601
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8300
8602
  children: [
8301
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Maximize2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8603
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Maximize2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8302
8604
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Fit to view" })
8303
8605
  ]
8304
8606
  }
@@ -8317,7 +8619,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8317
8619
  },
8318
8620
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8319
8621
  children: [
8320
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Search, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8622
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Search, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8321
8623
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Find on canvas" })
8322
8624
  ]
8323
8625
  }
@@ -8332,7 +8634,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8332
8634
  onClick: onOpenNotes,
8333
8635
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8334
8636
  children: [
8335
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.MessageCircle, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8637
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.MessageCircle, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8336
8638
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "flex-1 font-normal", children: [
8337
8639
  "View notes (",
8338
8640
  notesCount,
@@ -8343,7 +8645,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8343
8645
  ),
8344
8646
  isDevMode && onSaveLayout && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(DropdownMenu.Sub, { children: [
8345
8647
  /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(DropdownMenu.SubTrigger, { className: "flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none", children: [
8346
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Save, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8648
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Save, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8347
8649
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Layout" }),
8348
8650
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "text-[10px] text-amber-600 font-medium", children: "DEV" }),
8349
8651
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
@@ -8379,7 +8681,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8379
8681
  disabled: layoutStatus !== "idle",
8380
8682
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
8381
8683
  children: [
8382
- layoutStatus === "saving" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Loader2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Save, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8684
+ layoutStatus === "saving" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Loader2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Save, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8383
8685
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: layoutStatus === "saving" ? "Saving..." : "Save Layout" })
8384
8686
  ]
8385
8687
  }
@@ -8391,7 +8693,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8391
8693
  disabled: layoutStatus !== "idle",
8392
8694
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
8393
8695
  children: [
8394
- layoutStatus === "resetting" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Loader2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.RotateCcw, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8696
+ layoutStatus === "resetting" ? /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Loader2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.RotateCcw, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8395
8697
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: layoutStatus === "resetting" ? "Resetting..." : "Reset Layout" })
8396
8698
  ]
8397
8699
  }
@@ -8408,7 +8710,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8408
8710
  onClick: openChat,
8409
8711
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8410
8712
  children: [
8411
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Sparkles, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8713
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Sparkles, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8412
8714
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Ask a question" })
8413
8715
  ]
8414
8716
  }
@@ -8421,7 +8723,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8421
8723
  onClick: handleCopyArchitectureCode,
8422
8724
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8423
8725
  children: [
8424
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Code, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8726
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Code, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8425
8727
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Copy as mermaid" })
8426
8728
  ]
8427
8729
  }
@@ -8443,7 +8745,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8443
8745
  onClick: () => setIsShareModalOpen(true),
8444
8746
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8445
8747
  children: [
8446
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.Share2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8748
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.Share2, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8447
8749
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Share Link" })
8448
8750
  ]
8449
8751
  }
@@ -8467,7 +8769,7 @@ var VisualizerDropdownContent = (0, import_react74.memo)(
8467
8769
  onClick: openStudioModal,
8468
8770
  className: "px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors",
8469
8771
  children: [
8470
- /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react29.ExternalLink, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8772
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react30.ExternalLink, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8471
8773
  /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("span", { className: "flex-1 font-normal", children: "Open in EventCatalog Studio" })
8472
8774
  ]
8473
8775
  }
@@ -9010,30 +9312,144 @@ function flatLayout(nodes, edges, graphOpts, nodeSize, style) {
9010
9312
  return { nodes: layoutNodes, edges: layoutEdges };
9011
9313
  }
9012
9314
 
9315
+ // src/utils/local-packing.ts
9316
+ var toNumber = (value) => {
9317
+ if (typeof value === "number") return value;
9318
+ if (typeof value === "string") {
9319
+ const parsed = Number.parseFloat(value);
9320
+ return Number.isFinite(parsed) ? parsed : void 0;
9321
+ }
9322
+ return void 0;
9323
+ };
9324
+ var getPackableNodeSize = (node) => ({
9325
+ width: toNumber(node.measured?.width) ?? toNumber(node.width) ?? toNumber(node.style?.width) ?? 260,
9326
+ height: toNumber(node.measured?.height) ?? toNumber(node.height) ?? toNumber(node.style?.height) ?? 140
9327
+ });
9328
+ var rectsIntersect = (a, b, gap = 0) => a.x < b.x + b.width + gap && a.x + a.width + gap > b.x && a.y < b.y + b.height + gap && a.y + a.height + gap > b.y;
9329
+ var toRect = (node, y = node.position.y) => {
9330
+ const size = getPackableNodeSize(node);
9331
+ return {
9332
+ x: node.position.x,
9333
+ y,
9334
+ width: size.width,
9335
+ height: size.height
9336
+ };
9337
+ };
9338
+ var packNodesAroundBounds = ({
9339
+ nodes,
9340
+ movableNodeIds,
9341
+ protectedBounds,
9342
+ groupNodeId,
9343
+ gap = 40
9344
+ }) => {
9345
+ const movableNodes = nodes.filter((node) => movableNodeIds.has(node.id)).sort((a, b) => a.position.y - b.position.y);
9346
+ const movableIds = new Set(movableNodes.map((node) => node.id));
9347
+ const occupiedRects = [
9348
+ protectedBounds,
9349
+ ...nodes.filter(
9350
+ (node) => !node.parentId && node.id !== groupNodeId && !movableIds.has(node.id)
9351
+ ).map((node) => toRect(node))
9352
+ ];
9353
+ const plannedPositions = /* @__PURE__ */ new Map();
9354
+ const groupCenterY = protectedBounds.y + protectedBounds.height / 2;
9355
+ const placeNode = (node) => {
9356
+ const size = getPackableNodeSize(node);
9357
+ const nodeCenterY = node.position.y + size.height / 2;
9358
+ const moveDirection = nodeCenterY >= groupCenterY ? 1 : -1;
9359
+ let y = moveDirection > 0 ? Math.max(
9360
+ node.position.y,
9361
+ protectedBounds.y + protectedBounds.height + gap
9362
+ ) : Math.min(node.position.y, protectedBounds.y - size.height - gap);
9363
+ let attempts = 0;
9364
+ while (attempts < occupiedRects.length + 8) {
9365
+ const rect = {
9366
+ x: node.position.x,
9367
+ y,
9368
+ width: size.width,
9369
+ height: size.height
9370
+ };
9371
+ const collision = occupiedRects.find(
9372
+ (occupied) => rectsIntersect(rect, occupied, gap)
9373
+ );
9374
+ if (!collision) {
9375
+ occupiedRects.push(rect);
9376
+ plannedPositions.set(node.id, { ...node.position, y });
9377
+ return;
9378
+ }
9379
+ y = moveDirection > 0 ? collision.y + collision.height + gap : collision.y - size.height - gap;
9380
+ attempts += 1;
9381
+ }
9382
+ occupiedRects.push({
9383
+ x: node.position.x,
9384
+ y,
9385
+ width: size.width,
9386
+ height: size.height
9387
+ });
9388
+ plannedPositions.set(node.id, { ...node.position, y });
9389
+ };
9390
+ const below = movableNodes.filter((node) => {
9391
+ const size = getPackableNodeSize(node);
9392
+ return node.position.y + size.height / 2 >= groupCenterY;
9393
+ });
9394
+ const belowIds = new Set(below.map((node) => node.id));
9395
+ const above = movableNodes.filter((node) => !belowIds.has(node.id)).reverse();
9396
+ below.forEach(placeNode);
9397
+ above.forEach(placeNode);
9398
+ return plannedPositions;
9399
+ };
9400
+
9401
+ // src/utils/message-group-expansion.ts
9402
+ var getExpandedMessageGroupNode = (nodes, groupNodeId) => nodes.find(
9403
+ (node) => node.id === groupNodeId && node.type === "messageGroupExpanded"
9404
+ );
9405
+ var buildMessageGroupExpansionNodes = ({
9406
+ currentNodes,
9407
+ groupNodeId,
9408
+ expandedContainerNode,
9409
+ childNodes,
9410
+ downstreamNodes,
9411
+ getDownstreamPosition
9412
+ }) => {
9413
+ const withoutExistingGroup = currentNodes.filter(
9414
+ (node) => node.id !== groupNodeId && node.parentId !== groupNodeId
9415
+ );
9416
+ const existingIds = new Set(withoutExistingGroup.map((node) => node.id));
9417
+ const newDownstream = downstreamNodes.filter((node) => !existingIds.has(node.id)).map((node, index) => ({
9418
+ ...node,
9419
+ position: getDownstreamPosition(node, index)
9420
+ }));
9421
+ return [
9422
+ ...withoutExistingGroup,
9423
+ expandedContainerNode,
9424
+ ...childNodes,
9425
+ ...newDownstream
9426
+ ];
9427
+ };
9428
+
9013
9429
  // src/components/NotesToolbarButton.tsx
9014
9430
  var import_react77 = require("react");
9015
- var import_lucide_react30 = require("lucide-react");
9431
+ var import_lucide_react31 = require("lucide-react");
9016
9432
  var import_react78 = require("@xyflow/react");
9017
9433
  var Dialog4 = __toESM(require("@radix-ui/react-dialog"));
9018
9434
  var import_jsx_runtime42 = require("react/jsx-runtime");
9019
9435
  var NODE_TYPE_META = {
9020
- service: { icon: import_lucide_react30.ServerIcon, color: "#ec4899", label: "Service" },
9021
- services: { icon: import_lucide_react30.ServerIcon, color: "#ec4899", label: "Service" },
9022
- event: { icon: import_lucide_react30.Zap, color: "#f97316", label: "Event" },
9023
- events: { icon: import_lucide_react30.Zap, color: "#f97316", label: "Event" },
9024
- command: { icon: import_lucide_react30.MessageSquare, color: "#3b82f6", label: "Command" },
9025
- commands: { icon: import_lucide_react30.MessageSquare, color: "#3b82f6", label: "Command" },
9026
- query: { icon: import_lucide_react30.Search, color: "#22c55e", label: "Query" },
9027
- queries: { icon: import_lucide_react30.Search, color: "#22c55e", label: "Query" },
9028
- channel: { icon: import_lucide_react30.ArrowRightLeft, color: "#6b7280", label: "Channel" },
9029
- channels: { icon: import_lucide_react30.ArrowRightLeft, color: "#6b7280", label: "Channel" },
9030
- data: { icon: import_lucide_react30.Database, color: "#3b82f6", label: "Data" },
9031
- "data-products": { icon: import_lucide_react30.Package, color: "#6366f1", label: "Data Product" },
9032
- externalSystem: { icon: import_lucide_react30.Globe, color: "#ec4899", label: "External System" },
9033
- actor: { icon: import_lucide_react30.User, color: "#eab308", label: "Actor" },
9034
- view: { icon: import_lucide_react30.MonitorIcon, color: "#8b5cf6", label: "View" },
9035
- domain: { icon: import_lucide_react30.BoxesIcon, color: "#14b8a6", label: "Domain" },
9036
- domains: { icon: import_lucide_react30.BoxesIcon, color: "#14b8a6", label: "Domain" }
9436
+ service: { icon: import_lucide_react31.ServerIcon, color: "#ec4899", label: "Service" },
9437
+ services: { icon: import_lucide_react31.ServerIcon, color: "#ec4899", label: "Service" },
9438
+ event: { icon: import_lucide_react31.Zap, color: "#f97316", label: "Event" },
9439
+ events: { icon: import_lucide_react31.Zap, color: "#f97316", label: "Event" },
9440
+ command: { icon: import_lucide_react31.MessageSquare, color: "#3b82f6", label: "Command" },
9441
+ commands: { icon: import_lucide_react31.MessageSquare, color: "#3b82f6", label: "Command" },
9442
+ query: { icon: import_lucide_react31.Search, color: "#22c55e", label: "Query" },
9443
+ queries: { icon: import_lucide_react31.Search, color: "#22c55e", label: "Query" },
9444
+ channel: { icon: import_lucide_react31.ArrowRightLeft, color: "#6b7280", label: "Channel" },
9445
+ channels: { icon: import_lucide_react31.ArrowRightLeft, color: "#6b7280", label: "Channel" },
9446
+ data: { icon: import_lucide_react31.Database, color: "#3b82f6", label: "Data" },
9447
+ "data-products": { icon: import_lucide_react31.Package, color: "#6366f1", label: "Data Product" },
9448
+ externalSystem: { icon: import_lucide_react31.Globe, color: "#ec4899", label: "External System" },
9449
+ actor: { icon: import_lucide_react31.User, color: "#eab308", label: "Actor" },
9450
+ view: { icon: import_lucide_react31.MonitorIcon, color: "#8b5cf6", label: "View" },
9451
+ domain: { icon: import_lucide_react31.BoxesIcon, color: "#14b8a6", label: "Domain" },
9452
+ domains: { icon: import_lucide_react31.BoxesIcon, color: "#14b8a6", label: "Domain" }
9037
9453
  };
9038
9454
  function getNodeMeta(nodeType) {
9039
9455
  if (!nodeType) return null;
@@ -9172,7 +9588,7 @@ function PriorityBadge({ priority }) {
9172
9588
  lineHeight: 1.4
9173
9589
  },
9174
9590
  children: [
9175
- isUrgent && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react30.AlertTriangle, { style: { width: 9, height: 9 }, strokeWidth: 2.5 }),
9591
+ isUrgent && /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react31.AlertTriangle, { style: { width: 9, height: 9 }, strokeWidth: 2.5 }),
9176
9592
  p.label
9177
9593
  ]
9178
9594
  }
@@ -9367,7 +9783,7 @@ function AllNotesModal({
9367
9783
  flexShrink: 0
9368
9784
  },
9369
9785
  children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
9370
- import_lucide_react30.MessageCircle,
9786
+ import_lucide_react31.MessageCircle,
9371
9787
  {
9372
9788
  style: { width: 16, height: 16, color: "#64748b" },
9373
9789
  strokeWidth: 2.5
@@ -9428,7 +9844,7 @@ function AllNotesModal({
9428
9844
  flexShrink: 0
9429
9845
  },
9430
9846
  "aria-label": "Close",
9431
- children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react30.X, { style: { width: 15, height: 15 } })
9847
+ children: /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react31.X, { style: { width: 15, height: 15 } })
9432
9848
  }
9433
9849
  ) })
9434
9850
  ]
@@ -9448,7 +9864,7 @@ function AllNotesModal({
9448
9864
  children: noteGroups.map((group, i) => {
9449
9865
  const isActive = i === selectedIdx;
9450
9866
  const meta = getNodeMeta(group.nodeType);
9451
- const IconComp = meta?.icon || import_lucide_react30.MessageCircle;
9867
+ const IconComp = meta?.icon || import_lucide_react31.MessageCircle;
9452
9868
  const iconColor = meta?.color || "#64748b";
9453
9869
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
9454
9870
  "button",
@@ -9537,7 +9953,7 @@ function AllNotesModal({
9537
9953
  )
9538
9954
  ] }),
9539
9955
  /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
9540
- import_lucide_react30.ChevronRight,
9956
+ import_lucide_react31.ChevronRight,
9541
9957
  {
9542
9958
  style: {
9543
9959
  width: 14,
@@ -9589,7 +10005,7 @@ function AllNotesModal({
9589
10005
  children: [
9590
10006
  (() => {
9591
10007
  const meta = getNodeMeta(selected.nodeType);
9592
- const Icon = meta?.icon || import_lucide_react30.MessageCircle;
10008
+ const Icon = meta?.icon || import_lucide_react31.MessageCircle;
9593
10009
  const color = meta?.color || "#64748b";
9594
10010
  return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
9595
10011
  "div",
@@ -9680,7 +10096,7 @@ function AllNotesModal({
9680
10096
  e.currentTarget.style.color = "#475569";
9681
10097
  },
9682
10098
  children: [
9683
- /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react30.Locate, { style: { width: 12, height: 12 } }),
10099
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(import_lucide_react31.Locate, { style: { width: 12, height: 12 } }),
9684
10100
  "Find on canvas"
9685
10101
  ]
9686
10102
  }
@@ -9932,7 +10348,7 @@ var NodeGraphBuilder = ({
9932
10348
  );
9933
10349
  const [nodes, setNodes, onNodesChange] = (0, import_react80.useNodesState)(initialNodes);
9934
10350
  const [edges, setEdges, onEdgesChange] = (0, import_react80.useEdgesState)(initialEdges);
9935
- const { fitView, getNodes } = (0, import_react80.useReactFlow)();
10351
+ const { fitView, getNodes, getIntersectingNodes, getZoom, setCenter } = (0, import_react80.useReactFlow)();
9936
10352
  (0, import_react79.useEffect)(() => {
9937
10353
  setNodes(initialNodes);
9938
10354
  setEdges(initialEdges);
@@ -10072,57 +10488,107 @@ var NodeGraphBuilder = ({
10072
10488
  wrapper.classList.add("ec-animating-layout");
10073
10489
  setTimeout(() => wrapper.classList.remove("ec-animating-layout"), 400);
10074
10490
  }, []);
10075
- const relayoutGraph = (0, import_react79.useCallback)((nextNodes, nextEdges) => {
10076
- const g = new import_dagre3.default.graphlib.Graph({ compound: true });
10077
- g.setGraph({ rankdir: "LR", ranksep: 300, nodesep: 50 });
10078
- g.setDefaultEdgeLabel(() => ({}));
10079
- nextNodes.forEach((node) => {
10080
- if (node.parentId) return;
10081
- const w = node.style?.width || (node.type === "messageGroupExpanded" ? 380 : 150);
10082
- const h = node.style?.height || (node.type === "messageGroupExpanded" ? 0 : 120);
10083
- if (node.type === "messageGroupExpanded") {
10084
- const children = nextNodes.filter((n) => n.parentId === node.id);
10085
- const childHeight = children.length * 190 + 100;
10086
- g.setNode(node.id, { width: w, height: childHeight });
10087
- } else {
10088
- g.setNode(node.id, { width: w, height: h });
10089
- }
10090
- });
10091
- nextEdges.forEach((edge) => {
10092
- const sourceNode = nextNodes.find((n) => n.id === edge.source);
10093
- const targetNode = nextNodes.find((n) => n.id === edge.target);
10094
- const sourceTop = sourceNode?.parentId || edge.source;
10095
- const targetTop = targetNode?.parentId || edge.target;
10096
- if (g.hasNode(sourceTop) && g.hasNode(targetTop) && sourceTop !== targetTop) {
10097
- g.setEdge(sourceTop, targetTop);
10098
- }
10099
- });
10100
- import_dagre3.default.layout(g);
10101
- const positioned = nextNodes.map((node) => {
10102
- if (node.parentId) {
10103
- const parent = nextNodes.find((n) => n.id === node.parentId);
10104
- if (parent?.type === "flowExpanded") {
10105
- return node;
10491
+ const relayoutGraph = (0, import_react79.useCallback)(
10492
+ (nextNodes, nextEdges, anchor) => {
10493
+ const g = new import_dagre3.default.graphlib.Graph({ compound: true });
10494
+ g.setGraph({ rankdir: "LR", ranksep: 300, nodesep: 50 });
10495
+ g.setDefaultEdgeLabel(() => ({}));
10496
+ nextNodes.forEach((node) => {
10497
+ if (node.parentId) return;
10498
+ const w = node.style?.width || (node.type === "messageGroupExpanded" ? 380 : 150);
10499
+ const h = node.style?.height || (node.type === "messageGroupExpanded" ? 0 : 120);
10500
+ if (node.type === "messageGroupExpanded") {
10501
+ const children = nextNodes.filter((n) => n.parentId === node.id);
10502
+ const childHeight = children.length * 190 + 100;
10503
+ g.setNode(node.id, { width: w, height: childHeight });
10504
+ } else {
10505
+ g.setNode(node.id, { width: w, height: h });
10506
+ }
10507
+ });
10508
+ nextEdges.forEach((edge) => {
10509
+ const sourceNode = nextNodes.find((n) => n.id === edge.source);
10510
+ const targetNode = nextNodes.find((n) => n.id === edge.target);
10511
+ const sourceTop = sourceNode?.parentId || edge.source;
10512
+ const targetTop = targetNode?.parentId || edge.target;
10513
+ if (g.hasNode(sourceTop) && g.hasNode(targetTop) && sourceTop !== targetTop) {
10514
+ g.setEdge(sourceTop, targetTop);
10106
10515
  }
10107
- const parentWidth = parent?.style?.width || 380;
10108
- const childWidth = 240;
10109
- const xOffset = Math.max(20, (parentWidth - childWidth) / 2);
10110
- const siblings = nextNodes.filter((n) => n.parentId === node.parentId);
10111
- const index = siblings.indexOf(node);
10516
+ });
10517
+ import_dagre3.default.layout(g);
10518
+ const positioned = nextNodes.map((node) => {
10519
+ if (node.parentId) {
10520
+ const parent = nextNodes.find((n) => n.id === node.parentId);
10521
+ if (parent?.type === "flowExpanded") {
10522
+ return node;
10523
+ }
10524
+ const parentWidth = parent?.style?.width || 380;
10525
+ const childWidth = 240;
10526
+ const xOffset = Math.max(20, (parentWidth - childWidth) / 2);
10527
+ const siblings = nextNodes.filter(
10528
+ (n) => n.parentId === node.parentId
10529
+ );
10530
+ const index = siblings.indexOf(node);
10531
+ return {
10532
+ ...node,
10533
+ position: { x: xOffset, y: 70 + index * 190 }
10534
+ };
10535
+ }
10536
+ const pos = g.node(node.id);
10537
+ if (!pos) return node;
10112
10538
  return {
10113
10539
  ...node,
10114
- position: { x: xOffset, y: 70 + index * 190 }
10540
+ position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 }
10115
10541
  };
10116
- }
10117
- const pos = g.node(node.id);
10118
- if (!pos) return node;
10119
- return {
10120
- ...node,
10121
- position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 }
10542
+ });
10543
+ if (!anchor) return positioned;
10544
+ const positionedAnchor = positioned.find((node) => node.id === anchor.id);
10545
+ if (!positionedAnchor) return positioned;
10546
+ const offset = {
10547
+ x: anchor.position.x - positionedAnchor.position.x,
10548
+ y: anchor.position.y - positionedAnchor.position.y
10122
10549
  };
10123
- });
10124
- return positioned;
10125
- }, []);
10550
+ return positioned.map((node) => {
10551
+ if (node.parentId) return node;
10552
+ return {
10553
+ ...node,
10554
+ position: {
10555
+ x: node.position.x + offset.x,
10556
+ y: node.position.y + offset.y
10557
+ }
10558
+ };
10559
+ });
10560
+ },
10561
+ []
10562
+ );
10563
+ const makeRoomForRenderedExpandedGroup = (0, import_react79.useCallback)(
10564
+ (groupNodeId, groupBounds) => {
10565
+ const padding = 80;
10566
+ const protectedBounds = {
10567
+ x: groupBounds.x - padding,
10568
+ y: groupBounds.y - padding,
10569
+ width: groupBounds.width + padding * 2,
10570
+ height: groupBounds.height + padding * 2
10571
+ };
10572
+ const intersectingIds = new Set(
10573
+ getIntersectingNodes(protectedBounds, true).filter((node) => node.id !== groupNodeId && !node.parentId).map((node) => node.id)
10574
+ );
10575
+ if (intersectingIds.size === 0) return;
10576
+ setNodes((currentNodes) => {
10577
+ const plannedPositions = packNodesAroundBounds({
10578
+ nodes: currentNodes,
10579
+ movableNodeIds: intersectingIds,
10580
+ protectedBounds,
10581
+ groupNodeId
10582
+ });
10583
+ return currentNodes.map((node) => {
10584
+ const plannedPosition = plannedPositions.get(node.id);
10585
+ if (!plannedPosition) return node;
10586
+ return { ...node, position: plannedPosition };
10587
+ });
10588
+ });
10589
+ },
10590
+ [getIntersectingNodes, setNodes]
10591
+ );
10126
10592
  const layoutSubFlowChildren = (0, import_react79.useCallback)(
10127
10593
  (children, edges2, sizeOf, opts) => {
10128
10594
  const { padding, headerH, fallbackW = 240, fallbackH = 120 } = opts;
@@ -10220,9 +10686,19 @@ var NodeGraphBuilder = ({
10220
10686
  }
10221
10687
  const without = prev.filter(
10222
10688
  (n) => n.id !== groupNodeId && !childNodeIds.has(n.id) && !(downstreamNodeIds.has(n.id) && !referencedByEdges.has(n.id))
10223
- );
10224
- const next = [...without, originalNode];
10225
- return relayoutGraph(next, nextEdges);
10689
+ ).map((n) => {
10690
+ const stashedPosition = stashed?.nodePositions?.[n.id];
10691
+ if (!stashedPosition || n.parentId) return n;
10692
+ return { ...n, position: stashedPosition };
10693
+ });
10694
+ const next = [
10695
+ ...without,
10696
+ {
10697
+ ...originalNode,
10698
+ position: stashed?.nodePositions?.[groupNodeId] ?? expandedNode.position
10699
+ }
10700
+ ];
10701
+ return next;
10226
10702
  });
10227
10703
  setEdges((prev) => {
10228
10704
  const without = prev.filter(
@@ -10230,9 +10706,6 @@ var NodeGraphBuilder = ({
10230
10706
  );
10231
10707
  return [...without, ...originalEdges];
10232
10708
  });
10233
- requestAnimationFrame(() => {
10234
- fitView({ duration: 400, padding: 0.2 });
10235
- });
10236
10709
  },
10237
10710
  [
10238
10711
  initialNodes,
@@ -10240,8 +10713,7 @@ var NodeGraphBuilder = ({
10240
10713
  setNodes,
10241
10714
  setEdges,
10242
10715
  relayoutGraph,
10243
- animateLayout,
10244
- fitView
10716
+ animateLayout
10245
10717
  ]
10246
10718
  );
10247
10719
  (0, import_react79.useEffect)(() => {
@@ -10324,9 +10796,6 @@ var NodeGraphBuilder = ({
10324
10796
  onNodeClick(node);
10325
10797
  return;
10326
10798
  }
10327
- if (linksToVisualiser && onNavigate) {
10328
- return;
10329
- }
10330
10799
  const isFlow = edgesRef.current.some(
10331
10800
  (edge) => edge.type === "flow-edge"
10332
10801
  );
@@ -10340,6 +10809,24 @@ var NodeGraphBuilder = ({
10340
10809
  if (node.type === "messageGroup") {
10341
10810
  const groupData = node.data;
10342
10811
  const groupNodeId = node.id;
10812
+ const currentGroupNode = getExpandedMessageGroupNode(
10813
+ nodesRef.current,
10814
+ groupNodeId
10815
+ );
10816
+ if (currentGroupNode?.type === "messageGroupExpanded") {
10817
+ const measured = currentGroupNode?.measured;
10818
+ const width = measured?.width ?? currentGroupNode.style?.width ?? 380;
10819
+ const height = measured?.height ?? currentGroupNode.style?.height ?? 300;
10820
+ setCenter(
10821
+ currentGroupNode.position.x + width / 2,
10822
+ currentGroupNode.position.y + height / 2,
10823
+ {
10824
+ duration: 300,
10825
+ zoom: Math.min(Math.max(getZoom(), 0.55), 1)
10826
+ }
10827
+ );
10828
+ return;
10829
+ }
10343
10830
  const serviceNodeId = `${groupData.service.id}-${groupData.service.version}`;
10344
10831
  const childCount = groupData.messages?.length || 0;
10345
10832
  const containerWidth = 380;
@@ -10348,6 +10835,9 @@ var NodeGraphBuilder = ({
10348
10835
  (e) => e.source === groupNodeId || e.target === groupNodeId
10349
10836
  );
10350
10837
  const preExpansionNodeIds = nodesRef.current.map((n) => n.id);
10838
+ const preExpansionNodePositions = Object.fromEntries(
10839
+ nodesRef.current.map((n) => [n.id, { ...n.position }])
10840
+ );
10351
10841
  const expandedContainerNode = {
10352
10842
  id: groupNodeId,
10353
10843
  type: "messageGroupExpanded",
@@ -10360,7 +10850,8 @@ var NodeGraphBuilder = ({
10360
10850
  __preExpansion: {
10361
10851
  node,
10362
10852
  edges: preExpansionEdges,
10363
- nodeIds: preExpansionNodeIds
10853
+ nodeIds: preExpansionNodeIds,
10854
+ nodePositions: preExpansionNodePositions
10364
10855
  }
10365
10856
  },
10366
10857
  style: {
@@ -10421,24 +10912,19 @@ var NodeGraphBuilder = ({
10421
10912
  });
10422
10913
  animateLayout();
10423
10914
  setNodes((prev) => {
10424
- const without = prev.filter((n) => n.id !== groupNodeId);
10425
- const existingIds = new Set(without.map((n) => n.id));
10426
- const newDownstream = downstreamNodes.filter(
10427
- (n) => !existingIds.has(n.id)
10428
- );
10429
- const next = [
10430
- ...without,
10915
+ const downstreamX = groupData.direction === "sends" ? node.position.x + containerWidth + 260 : node.position.x - 420;
10916
+ const downstreamY = node.position.y + 40;
10917
+ return buildMessageGroupExpansionNodes({
10918
+ currentNodes: prev,
10919
+ groupNodeId,
10431
10920
  expandedContainerNode,
10432
- ...childNodes,
10433
- ...newDownstream
10434
- ];
10435
- return relayoutGraph(next, [
10436
- ...edgesRef.current.filter(
10437
- (e) => e.source !== groupNodeId && e.target !== groupNodeId
10438
- ),
10439
- ...childEdges,
10440
- ...downstreamEdges
10441
- ]);
10921
+ childNodes,
10922
+ downstreamNodes,
10923
+ getDownstreamPosition: (_downstreamNode, index) => ({
10924
+ x: downstreamX,
10925
+ y: downstreamY + index * 190
10926
+ })
10927
+ });
10442
10928
  });
10443
10929
  setEdges((prev) => {
10444
10930
  const without = prev.filter(
@@ -10447,6 +10933,12 @@ var NodeGraphBuilder = ({
10447
10933
  return [...without, ...childEdges, ...downstreamEdges];
10448
10934
  });
10449
10935
  requestAnimationFrame(() => {
10936
+ let actualContainerBounds = {
10937
+ x: node.position.x,
10938
+ y: node.position.y,
10939
+ width: containerWidth,
10940
+ height: containerHeight
10941
+ };
10450
10942
  setNodes((prev) => {
10451
10943
  const children = prev.filter((n) => n.parentId === groupNodeId);
10452
10944
  if (children.length === 0) return prev;
@@ -10466,8 +10958,20 @@ var NodeGraphBuilder = ({
10466
10958
  const totalChildH = measurements.reduce((sum, m) => sum + m.h, 0) + gap * (measurements.length - 1);
10467
10959
  const actualContainerH = headerH + totalChildH + padding * 2;
10468
10960
  let currentY = headerH + padding;
10961
+ actualContainerBounds = {
10962
+ x: node.position.x,
10963
+ y: node.position.y,
10964
+ width: containerWidth,
10965
+ height: actualContainerH
10966
+ };
10469
10967
  return prev.map((n) => {
10470
10968
  if (n.id === groupNodeId) {
10969
+ actualContainerBounds = {
10970
+ x: n.position.x,
10971
+ y: n.position.y,
10972
+ width: containerWidth,
10973
+ height: actualContainerH
10974
+ };
10471
10975
  return {
10472
10976
  ...n,
10473
10977
  style: {
@@ -10485,9 +10989,23 @@ var NodeGraphBuilder = ({
10485
10989
  return { ...n, position: { x, y } };
10486
10990
  });
10487
10991
  });
10488
- });
10489
- requestAnimationFrame(() => {
10490
- fitView({ duration: 400, padding: 0.2 });
10992
+ requestAnimationFrame(() => {
10993
+ const groupNode = getNodes().find((n) => n.id === groupNodeId);
10994
+ const measured = groupNode?.measured;
10995
+ const width = measured?.width ?? groupNode?.style?.width ?? containerWidth;
10996
+ const height = measured?.height ?? groupNode?.style?.height ?? actualContainerBounds.height;
10997
+ const bounds = {
10998
+ x: groupNode?.position.x ?? actualContainerBounds.x,
10999
+ y: groupNode?.position.y ?? actualContainerBounds.y,
11000
+ width,
11001
+ height
11002
+ };
11003
+ makeRoomForRenderedExpandedGroup(groupNodeId, bounds);
11004
+ setCenter(bounds.x + width / 2, bounds.y + height / 2, {
11005
+ duration: 450,
11006
+ zoom: Math.min(Math.max(getZoom(), 0.55), 1)
11007
+ });
11008
+ });
10491
11009
  });
10492
11010
  return;
10493
11011
  }
@@ -10652,10 +11170,21 @@ var NodeGraphBuilder = ({
10652
11170
  });
10653
11171
  return;
10654
11172
  }
11173
+ if (linksToVisualiser && onNavigate) {
11174
+ return;
11175
+ }
10655
11176
  setFocusedNodeId(node.id);
10656
11177
  setFocusModeOpen(true);
10657
11178
  },
10658
- [onNodeClick, linksToVisualiser, onNavigate]
11179
+ [
11180
+ onNodeClick,
11181
+ linksToVisualiser,
11182
+ onNavigate,
11183
+ makeRoomForRenderedExpandedGroup,
11184
+ getNodes,
11185
+ getZoom,
11186
+ setCenter
11187
+ ]
10659
11188
  );
10660
11189
  const toggleAnimateMessages = (0, import_react79.useCallback)(() => {
10661
11190
  setAnimateMessages((prev) => {
@@ -10885,13 +11414,15 @@ var NodeGraphBuilder = ({
10885
11414
  }, [getNodes, downloadImage, title]);
10886
11415
  const handleLegendClick = (0, import_react79.useCallback)(
10887
11416
  (collectionType, groupId) => {
11417
+ const isLegendTarget = (node) => {
11418
+ if (groupId) {
11419
+ return node.data.group && node.data.group?.id === groupId;
11420
+ }
11421
+ return node.type === collectionType;
11422
+ };
10888
11423
  const updatedNodes = nodes.map((node) => {
10889
- if (groupId && node.data.group && node.data.group?.id === groupId) {
11424
+ if (isLegendTarget(node)) {
10890
11425
  return { ...node, style: { ...node.style, opacity: 1 } };
10891
- } else {
10892
- if (node.type === collectionType) {
10893
- return { ...node, style: { ...node.style, opacity: 1 } };
10894
- }
10895
11426
  }
10896
11427
  return { ...node, style: { ...node.style, opacity: 0.1 } };
10897
11428
  });
@@ -10906,10 +11437,12 @@ var NodeGraphBuilder = ({
10906
11437
  });
10907
11438
  setNodes(updatedNodes);
10908
11439
  setEdges(updatedEdges);
11440
+ const targetNodes = updatedNodes.filter(isLegendTarget);
11441
+ if (targetNodes.length === 0) return;
10909
11442
  fitView({
10910
11443
  padding: 0.2,
10911
11444
  duration: 800,
10912
- nodes: updatedNodes.filter((node) => node.type === collectionType)
11445
+ nodes: targetNodes
10913
11446
  });
10914
11447
  },
10915
11448
  [nodes, edges, setNodes, setEdges, fitView]
@@ -11106,7 +11639,7 @@ var NodeGraphBuilder = ({
11106
11639
  "aria-label": "Open menu",
11107
11640
  children: [
11108
11641
  title && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight", children: title }),
11109
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.MoreVertical, { className: "h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" })
11642
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.MoreVertical, { className: "h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" })
11110
11643
  ]
11111
11644
  }
11112
11645
  ) }),
@@ -11208,7 +11741,7 @@ var NodeGraphBuilder = ({
11208
11741
  "aria-label": "Open menu",
11209
11742
  children: [
11210
11743
  title && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight", children: title }),
11211
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.MoreVertical, { className: "h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" })
11744
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.MoreVertical, { className: "h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" })
11212
11745
  ]
11213
11746
  }
11214
11747
  ) }),
@@ -11270,7 +11803,7 @@ var NodeGraphBuilder = ({
11270
11803
  ) }) })
11271
11804
  ] }),
11272
11805
  links.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex justify-end mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "relative flex items-center -mt-1", children: [
11273
- /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute left-2 pointer-events-none flex items-center h-full", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.HistoryIcon, { className: "h-4 w-4 text-[rgb(var(--ec-page-text-muted))]" }) }),
11806
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "absolute left-2 pointer-events-none flex items-center h-full", children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.HistoryIcon, { className: "h-4 w-4 text-[rgb(var(--ec-page-text-muted))]" }) }),
11274
11807
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
11275
11808
  "select",
11276
11809
  {
@@ -11425,7 +11958,7 @@ var NodeGraphBuilder = ({
11425
11958
  onClick: () => setIsShareModalOpen(false),
11426
11959
  className: "text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors",
11427
11960
  "aria-label": "Close modal",
11428
- children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.ExternalLink, { className: "w-5 h-5 rotate-180" })
11961
+ children: /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.ExternalLink, { className: "w-5 h-5 rotate-180" })
11429
11962
  }
11430
11963
  )
11431
11964
  ] }),
@@ -11447,7 +11980,7 @@ var NodeGraphBuilder = ({
11447
11980
  className: `px-4 py-2.5 rounded-md font-medium transition-all duration-200 flex items-center gap-2 ${shareUrlCopySuccess ? "bg-green-500 text-white" : "bg-[rgb(var(--ec-accent))] text-white hover:opacity-90"}`,
11448
11981
  "aria-label": shareUrlCopySuccess ? "Copied!" : "Copy link",
11449
11982
  children: [
11450
- shareUrlCopySuccess ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.CheckIcon, { className: "w-4 h-4" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react31.ClipboardIcon, { className: "w-4 h-4" }),
11983
+ shareUrlCopySuccess ? /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.CheckIcon, { className: "w-4 h-4" }) : /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(import_lucide_react32.ClipboardIcon, { className: "w-4 h-4" }),
11451
11984
  /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { children: shareUrlCopySuccess ? "Copied!" : "Copy" })
11452
11985
  ]
11453
11986
  }