@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.mjs CHANGED
@@ -6043,7 +6043,75 @@ import {
6043
6043
  useImperativeHandle,
6044
6044
  memo as memo31
6045
6045
  } from "react";
6046
+ import {
6047
+ Blocks,
6048
+ Database as Database5,
6049
+ Layers as Layers3,
6050
+ ListTree,
6051
+ MessageSquare as MessageSquare2,
6052
+ Search as SearchIcon,
6053
+ Server as Server2,
6054
+ User as User3,
6055
+ Workflow as Workflow2,
6056
+ Zap as Zap4
6057
+ } from "lucide-react";
6046
6058
  import { jsx as jsx32, jsxs as jsxs30 } from "react/jsx-runtime";
6059
+ var formatVersionedName = (name, version) => {
6060
+ if (version) {
6061
+ const nameWithoutVersion = name.replace(
6062
+ new RegExp(`-v?${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
6063
+ ""
6064
+ );
6065
+ return `${nameWithoutVersion} (v${version})`;
6066
+ }
6067
+ const versionMatch = name.match(
6068
+ /^(.+)-v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/
6069
+ );
6070
+ if (!versionMatch) return name;
6071
+ return `${versionMatch[1]} (v${versionMatch[2]})`;
6072
+ };
6073
+ var normalizeCollectionType = (type) => {
6074
+ const aliases = {
6075
+ event: "events",
6076
+ command: "commands",
6077
+ query: "queries",
6078
+ service: "services",
6079
+ domain: "domains",
6080
+ channel: "channels",
6081
+ entity: "entities"
6082
+ };
6083
+ return aliases[type] || type;
6084
+ };
6085
+ var splitVersionedName = (name, version) => {
6086
+ if (version) {
6087
+ return {
6088
+ id: name.replace(
6089
+ new RegExp(`-v?${version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`),
6090
+ ""
6091
+ ),
6092
+ version
6093
+ };
6094
+ }
6095
+ const versionMatch = name.match(
6096
+ /^(.+)-v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/
6097
+ );
6098
+ if (!versionMatch) return { id: name, version: void 0 };
6099
+ return { id: versionMatch[1], version: versionMatch[2] };
6100
+ };
6101
+ var getResourceKey = ({
6102
+ type,
6103
+ id,
6104
+ name,
6105
+ version
6106
+ }) => {
6107
+ const parsed = splitVersionedName(id || name, version);
6108
+ return `${normalizeCollectionType(type)}:${parsed.id}:${parsed.version || ""}`.toLowerCase();
6109
+ };
6110
+ var getNodeResourceData = (data, key) => {
6111
+ const resource = data?.[key];
6112
+ if (!resource || typeof resource !== "object") return void 0;
6113
+ return "data" in resource && resource.data ? resource.data : resource;
6114
+ };
6047
6115
  var VisualiserSearch = memo31(
6048
6116
  forwardRef(
6049
6117
  ({ nodes, onNodeSelect, onClear, onPaneClick: _onPaneClick }, ref) => {
@@ -6053,6 +6121,8 @@ var VisualiserSearch = memo31(
6053
6121
  const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState5(-1);
6054
6122
  const searchInputRef = useRef2(null);
6055
6123
  const containerRef = useRef2(null);
6124
+ const suggestionsListRef = useRef2(null);
6125
+ const suggestionItemRefs = useRef2([]);
6056
6126
  const hideSuggestions = useCallback4(() => {
6057
6127
  setShowSuggestions(false);
6058
6128
  setSelectedSuggestionIndex(-1);
@@ -6071,84 +6141,277 @@ var VisualiserSearch = memo31(
6071
6141
  if (node.type === "messageGroupExpanded") {
6072
6142
  return node.data?.groupName || node.id;
6073
6143
  }
6074
- 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;
6075
- 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;
6076
- return version ? `${name} (v${version})` : name;
6144
+ const message = getNodeResourceData(node.data, "message");
6145
+ const service = getNodeResourceData(node.data, "service");
6146
+ const domain = getNodeResourceData(node.data, "domain");
6147
+ const entity = getNodeResourceData(node.data, "entity");
6148
+ const channel = getNodeResourceData(node.data, "channel");
6149
+ const dataProduct = getNodeResourceData(node.data, "dataProduct");
6150
+ const data = getNodeResourceData(node.data, "data");
6151
+ 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;
6152
+ const version = message?.version || service?.version || domain?.version || entity?.version || channel?.version || dataProduct?.version || data?.version || node.data?.version;
6153
+ return formatVersionedName(name, version);
6077
6154
  }, []);
6078
- const getNodeTypeColorClass = useCallback4((nodeType) => {
6079
- const colorClasses = {
6080
- events: "bg-orange-600 text-white",
6081
- services: "bg-pink-600 text-white",
6082
- flows: "bg-teal-600 text-white",
6083
- commands: "bg-blue-600 text-white",
6084
- queries: "bg-green-600 text-white",
6085
- channels: "bg-gray-600 text-white",
6086
- domains: "bg-yellow-500 text-white",
6087
- externalSystem: "bg-pink-600 text-white",
6088
- actor: "bg-yellow-500 text-white",
6089
- step: "bg-gray-700 text-white",
6090
- user: "bg-yellow-500 text-white",
6091
- custom: "bg-gray-500 text-white",
6092
- field: "bg-cyan-600 text-white",
6093
- messageGroup: "bg-violet-600 text-white",
6094
- messageGroupExpanded: "bg-violet-600 text-white"
6155
+ const getNodeResourceKey = useCallback4(
6156
+ (node, label) => {
6157
+ if (node.type === "messageGroup" || node.type === "messageGroupExpanded") {
6158
+ return `${node.type}:${node.id}`.toLowerCase();
6159
+ }
6160
+ const data = node.data;
6161
+ const resource = getNodeResourceData(data, "message") || getNodeResourceData(data, "service") || getNodeResourceData(data, "domain") || getNodeResourceData(data, "entity") || getNodeResourceData(data, "channel") || getNodeResourceData(data, "dataProduct") || data?.data || data;
6162
+ return getResourceKey({
6163
+ type: node.type || "unknown",
6164
+ id: resource?.id,
6165
+ name: resource?.name || resource?.id || label || node.id,
6166
+ version: resource?.version || data?.version
6167
+ });
6168
+ },
6169
+ []
6170
+ );
6171
+ const dedupeSearchSuggestions = useCallback4(
6172
+ (suggestions) => {
6173
+ const uniqueSuggestions = [];
6174
+ const indexByResourceKey = /* @__PURE__ */ new Map();
6175
+ suggestions.forEach((suggestion) => {
6176
+ const existingIndex = indexByResourceKey.get(
6177
+ suggestion.resourceKey
6178
+ );
6179
+ if (existingIndex === void 0) {
6180
+ indexByResourceKey.set(
6181
+ suggestion.resourceKey,
6182
+ uniqueSuggestions.length
6183
+ );
6184
+ uniqueSuggestions.push(suggestion);
6185
+ return;
6186
+ }
6187
+ if (suggestion.isGroupedMessage && !uniqueSuggestions[existingIndex].isGroupedMessage) {
6188
+ uniqueSuggestions[existingIndex] = suggestion;
6189
+ }
6190
+ });
6191
+ return uniqueSuggestions;
6192
+ },
6193
+ []
6194
+ );
6195
+ const getSearchSuggestions = useCallback4(
6196
+ (nodesToIndex) => {
6197
+ const suggestions = nodesToIndex.flatMap((node) => {
6198
+ const nodeName = getNodeDisplayName(node);
6199
+ const suggestions2 = [
6200
+ {
6201
+ key: node.id,
6202
+ node,
6203
+ label: nodeName,
6204
+ searchText: nodeName,
6205
+ type: node.type || "unknown",
6206
+ resourceKey: getNodeResourceKey(node, nodeName)
6207
+ }
6208
+ ];
6209
+ if (node.type !== "messageGroup") return suggestions2;
6210
+ const groupName = node.data?.groupName || nodeName;
6211
+ const groupedMessages = node.data?.messages || [];
6212
+ groupedMessages.forEach((item, index) => {
6213
+ const message = item.message;
6214
+ const messageName = message?.data?.name || message?.data?.id;
6215
+ if (!messageName) return;
6216
+ const version = message?.data?.version;
6217
+ const label = formatVersionedName(messageName, version);
6218
+ suggestions2.push({
6219
+ key: `${node.id}:${message?.data?.id || messageName}:${version || index}`,
6220
+ node,
6221
+ label,
6222
+ searchText: `${label} ${groupName}`,
6223
+ type: message?.collection || "message",
6224
+ resourceKey: getResourceKey({
6225
+ type: message?.collection || "message",
6226
+ id: message?.data?.id,
6227
+ name: messageName,
6228
+ version
6229
+ }),
6230
+ groupName,
6231
+ isGroupedMessage: true
6232
+ });
6233
+ });
6234
+ return suggestions2;
6235
+ });
6236
+ return dedupeSearchSuggestions(suggestions);
6237
+ },
6238
+ [dedupeSearchSuggestions, getNodeDisplayName, getNodeResourceKey]
6239
+ );
6240
+ const getNodeTypeMeta = useCallback4((nodeType) => {
6241
+ const meta = {
6242
+ events: {
6243
+ label: "Event",
6244
+ Icon: Zap4,
6245
+ iconClass: "border-orange-500/25 bg-orange-500/10 text-orange-500",
6246
+ badgeClass: "border-orange-500/25 bg-orange-500/10 text-orange-700 dark:text-orange-300"
6247
+ },
6248
+ event: {
6249
+ label: "Event",
6250
+ Icon: Zap4,
6251
+ iconClass: "border-orange-500/25 bg-orange-500/10 text-orange-500",
6252
+ badgeClass: "border-orange-500/25 bg-orange-500/10 text-orange-700 dark:text-orange-300"
6253
+ },
6254
+ commands: {
6255
+ label: "Command",
6256
+ Icon: MessageSquare2,
6257
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6258
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6259
+ },
6260
+ command: {
6261
+ label: "Command",
6262
+ Icon: MessageSquare2,
6263
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6264
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6265
+ },
6266
+ queries: {
6267
+ label: "Query",
6268
+ Icon: SearchIcon,
6269
+ iconClass: "border-green-500/25 bg-green-500/10 text-green-500",
6270
+ badgeClass: "border-green-500/25 bg-green-500/10 text-green-700 dark:text-green-300"
6271
+ },
6272
+ query: {
6273
+ label: "Query",
6274
+ Icon: SearchIcon,
6275
+ iconClass: "border-green-500/25 bg-green-500/10 text-green-500",
6276
+ badgeClass: "border-green-500/25 bg-green-500/10 text-green-700 dark:text-green-300"
6277
+ },
6278
+ services: {
6279
+ label: "Service",
6280
+ Icon: Server2,
6281
+ iconClass: "border-pink-500/25 bg-pink-500/10 text-pink-500",
6282
+ badgeClass: "border-pink-500/25 bg-pink-500/10 text-pink-700 dark:text-pink-300"
6283
+ },
6284
+ domains: {
6285
+ label: "Domain",
6286
+ Icon: Blocks,
6287
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6288
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6289
+ },
6290
+ flows: {
6291
+ label: "Flow",
6292
+ Icon: Workflow2,
6293
+ iconClass: "border-teal-500/25 bg-teal-500/10 text-teal-500",
6294
+ badgeClass: "border-teal-500/25 bg-teal-500/10 text-teal-700 dark:text-teal-300"
6295
+ },
6296
+ channels: {
6297
+ label: "Channel",
6298
+ Icon: ListTree,
6299
+ iconClass: "border-gray-500/25 bg-gray-500/10 text-gray-500",
6300
+ badgeClass: "border-gray-500/25 bg-gray-500/10 text-gray-700 dark:text-gray-300"
6301
+ },
6302
+ data: {
6303
+ label: "Data",
6304
+ Icon: Database5,
6305
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6306
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6307
+ },
6308
+ entities: {
6309
+ label: "Entity",
6310
+ Icon: Database5,
6311
+ iconClass: "border-blue-500/25 bg-blue-500/10 text-blue-500",
6312
+ badgeClass: "border-blue-500/25 bg-blue-500/10 text-blue-700 dark:text-blue-300"
6313
+ },
6314
+ externalSystem: {
6315
+ label: "External",
6316
+ Icon: Server2,
6317
+ iconClass: "border-pink-500/25 bg-pink-500/10 text-pink-500",
6318
+ badgeClass: "border-pink-500/25 bg-pink-500/10 text-pink-700 dark:text-pink-300"
6319
+ },
6320
+ actor: {
6321
+ label: "Actor",
6322
+ Icon: User3,
6323
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6324
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6325
+ },
6326
+ user: {
6327
+ label: "User",
6328
+ Icon: User3,
6329
+ iconClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
6330
+ badgeClass: "border-yellow-500/25 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300"
6331
+ },
6332
+ messageGroup: {
6333
+ label: "Group",
6334
+ Icon: Layers3,
6335
+ iconClass: "border-violet-500/25 bg-violet-500/10 text-violet-500",
6336
+ badgeClass: "border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300"
6337
+ },
6338
+ messageGroupExpanded: {
6339
+ label: "Group",
6340
+ Icon: Layers3,
6341
+ iconClass: "border-violet-500/25 bg-violet-500/10 text-violet-500",
6342
+ badgeClass: "border-violet-500/25 bg-violet-500/10 text-violet-700 dark:text-violet-300"
6343
+ }
6344
+ };
6345
+ return meta[nodeType] || {
6346
+ label: nodeType,
6347
+ Icon: Layers3,
6348
+ iconClass: "border-gray-500/25 bg-gray-500/10 text-gray-500",
6349
+ badgeClass: "border-gray-500/25 bg-gray-500/10 text-gray-700 dark:text-gray-300"
6095
6350
  };
6096
- return colorClasses[nodeType] || "bg-gray-100 text-gray-700";
6097
6351
  }, []);
6098
6352
  const handleSearchChange = useCallback4(
6099
6353
  (event) => {
6100
6354
  const query = event.target.value;
6101
6355
  setSearchQuery(query);
6102
6356
  if (query.length > 0) {
6103
- const filtered = nodes.filter((node) => {
6104
- const nodeName = getNodeDisplayName(node);
6105
- return nodeName.toLowerCase().includes(query.toLowerCase());
6106
- });
6357
+ const search = query.toLowerCase();
6358
+ const filtered = getSearchSuggestions(nodes).filter(
6359
+ (suggestion) => suggestion.searchText.toLowerCase().includes(search)
6360
+ );
6107
6361
  setFilteredSuggestions(filtered);
6108
6362
  setShowSuggestions(true);
6109
6363
  setSelectedSuggestionIndex(-1);
6110
6364
  } else {
6111
- setFilteredSuggestions(nodes);
6365
+ setFilteredSuggestions(getSearchSuggestions(nodes));
6112
6366
  setShowSuggestions(true);
6113
6367
  setSelectedSuggestionIndex(-1);
6114
6368
  }
6115
6369
  },
6116
- [nodes, getNodeDisplayName]
6370
+ [nodes, getSearchSuggestions]
6117
6371
  );
6118
6372
  const handleSearchFocus = useCallback4(() => {
6119
- if (searchQuery.length === 0) {
6120
- setFilteredSuggestions(nodes);
6121
- }
6373
+ const suggestions = getSearchSuggestions(nodes);
6374
+ const search = searchQuery.toLowerCase();
6375
+ setFilteredSuggestions(
6376
+ searchQuery.length === 0 ? suggestions : suggestions.filter(
6377
+ (suggestion) => suggestion.searchText.toLowerCase().includes(search)
6378
+ )
6379
+ );
6122
6380
  setShowSuggestions(true);
6123
6381
  setSelectedSuggestionIndex(-1);
6124
- }, [nodes, searchQuery]);
6382
+ }, [nodes, searchQuery, getSearchSuggestions]);
6125
6383
  const handleSuggestionClick = useCallback4(
6126
- (node) => {
6127
- setSearchQuery(getNodeDisplayName(node));
6384
+ (suggestion) => {
6385
+ setSearchQuery("");
6386
+ setFilteredSuggestions([]);
6128
6387
  setShowSuggestions(false);
6129
- onNodeSelect(node);
6388
+ setSelectedSuggestionIndex(-1);
6389
+ onNodeSelect(suggestion.node);
6130
6390
  },
6131
- [onNodeSelect, getNodeDisplayName]
6391
+ [onNodeSelect]
6132
6392
  );
6133
6393
  const handleSearchKeyDown = useCallback4(
6134
6394
  (event) => {
6135
- if (!showSuggestions || filteredSuggestions.length === 0) return;
6136
6395
  switch (event.key) {
6137
6396
  case "ArrowDown":
6138
6397
  event.preventDefault();
6398
+ if (filteredSuggestions.length === 0) return;
6399
+ setShowSuggestions(true);
6139
6400
  setSelectedSuggestionIndex(
6140
6401
  (prev) => prev < filteredSuggestions.length - 1 ? prev + 1 : 0
6141
6402
  );
6142
6403
  break;
6143
6404
  case "ArrowUp":
6144
6405
  event.preventDefault();
6406
+ if (filteredSuggestions.length === 0) return;
6407
+ setShowSuggestions(true);
6145
6408
  setSelectedSuggestionIndex(
6146
6409
  (prev) => prev > 0 ? prev - 1 : filteredSuggestions.length - 1
6147
6410
  );
6148
6411
  break;
6149
6412
  case "Enter":
6150
6413
  event.preventDefault();
6151
- if (selectedSuggestionIndex >= 0) {
6414
+ if (showSuggestions && selectedSuggestionIndex >= 0 && selectedSuggestionIndex < filteredSuggestions.length) {
6152
6415
  handleSuggestionClick(
6153
6416
  filteredSuggestions[selectedSuggestionIndex]
6154
6417
  );
@@ -6177,6 +6440,31 @@ var VisualiserSearch = memo31(
6177
6440
  searchInputRef.current.focus();
6178
6441
  }
6179
6442
  }, [onClear]);
6443
+ useEffect2(() => {
6444
+ suggestionItemRefs.current = suggestionItemRefs.current.slice(
6445
+ 0,
6446
+ filteredSuggestions.length
6447
+ );
6448
+ }, [filteredSuggestions.length]);
6449
+ useEffect2(() => {
6450
+ if (!showSuggestions || selectedSuggestionIndex < 0) return;
6451
+ const list = suggestionsListRef.current;
6452
+ const item = suggestionItemRefs.current[selectedSuggestionIndex];
6453
+ if (!list || !item) return;
6454
+ const itemTop = item.offsetTop;
6455
+ const itemBottom = itemTop + item.offsetHeight;
6456
+ const visibleTop = list.scrollTop;
6457
+ const visibleBottom = visibleTop + list.clientHeight;
6458
+ if (itemTop < visibleTop) {
6459
+ list.scrollTop = itemTop;
6460
+ } else if (itemBottom > visibleBottom) {
6461
+ list.scrollTop = itemBottom - list.clientHeight;
6462
+ }
6463
+ }, [
6464
+ showSuggestions,
6465
+ selectedSuggestionIndex,
6466
+ filteredSuggestions.length
6467
+ ]);
6180
6468
  useEffect2(() => {
6181
6469
  const handleClickOutside = (event) => {
6182
6470
  if (containerRef.current && !containerRef.current.contains(event.target)) {
@@ -6231,28 +6519,53 @@ var VisualiserSearch = memo31(
6231
6519
  }
6232
6520
  )
6233
6521
  ] }),
6234
- showSuggestions && filteredSuggestions.length > 0 && /* @__PURE__ */ jsx32("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) => {
6235
- const nodeName = getNodeDisplayName(node);
6236
- const nodeType = node.type || "unknown";
6237
- return /* @__PURE__ */ jsxs30(
6238
- "div",
6239
- {
6240
- onClick: () => handleSuggestionClick(node),
6241
- 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))]" : ""}`,
6242
- children: [
6243
- /* @__PURE__ */ jsx32("span", { className: "text-sm font-medium text-[rgb(var(--ec-page-text))]", children: nodeName }),
6244
- /* @__PURE__ */ jsx32(
6245
- "span",
6246
- {
6247
- className: `text-xs capitalize px-2 py-1 rounded ${getNodeTypeColorClass(nodeType)}`,
6248
- children: nodeType
6249
- }
6250
- )
6251
- ]
6252
- },
6253
- node.id
6254
- );
6255
- }) })
6522
+ showSuggestions && filteredSuggestions.length > 0 && /* @__PURE__ */ jsx32(
6523
+ "div",
6524
+ {
6525
+ ref: suggestionsListRef,
6526
+ 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",
6527
+ children: filteredSuggestions.map((suggestion, index) => {
6528
+ const nodeTypeMeta = getNodeTypeMeta(suggestion.type);
6529
+ const Icon = nodeTypeMeta.Icon;
6530
+ const isSelected = index === selectedSuggestionIndex;
6531
+ return /* @__PURE__ */ jsxs30(
6532
+ "div",
6533
+ {
6534
+ ref: (element) => {
6535
+ suggestionItemRefs.current[index] = element;
6536
+ },
6537
+ onClick: () => handleSuggestionClick(suggestion),
6538
+ onMouseEnter: () => setSelectedSuggestionIndex(index),
6539
+ 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)]"}`,
6540
+ children: [
6541
+ /* @__PURE__ */ jsx32(
6542
+ "span",
6543
+ {
6544
+ className: `mt-0.5 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border ${nodeTypeMeta.iconClass}`,
6545
+ children: /* @__PURE__ */ jsx32(Icon, { className: "h-3.5 w-3.5" })
6546
+ }
6547
+ ),
6548
+ /* @__PURE__ */ jsxs30("span", { className: "min-w-0 flex-1", children: [
6549
+ /* @__PURE__ */ jsx32("span", { className: "block truncate text-sm font-medium text-[rgb(var(--ec-page-text))]", children: suggestion.label }),
6550
+ suggestion.isGroupedMessage && suggestion.groupName && /* @__PURE__ */ jsxs30("span", { className: "mt-0.5 block truncate text-xs text-[rgb(var(--ec-page-text-muted))]", children: [
6551
+ "in ",
6552
+ suggestion.groupName
6553
+ ] })
6554
+ ] }),
6555
+ /* @__PURE__ */ jsx32(
6556
+ "span",
6557
+ {
6558
+ className: `mt-0.5 flex-shrink-0 rounded border px-2 py-0.5 text-xs font-medium ${nodeTypeMeta.badgeClass}`,
6559
+ children: nodeTypeMeta.label
6560
+ }
6561
+ )
6562
+ ]
6563
+ },
6564
+ `${suggestion.key}:${index}`
6565
+ );
6566
+ })
6567
+ }
6568
+ )
6256
6569
  ] });
6257
6570
  }
6258
6571
  )
@@ -8083,7 +8396,7 @@ import {
8083
8396
  Maximize2 as Maximize23,
8084
8397
  Map as Map2,
8085
8398
  Sparkles,
8086
- Zap as Zap4,
8399
+ Zap as Zap5,
8087
8400
  EyeOff,
8088
8401
  ExternalLink,
8089
8402
  Save,
@@ -8208,7 +8521,7 @@ var VisualizerDropdownContent = memo33(
8208
8521
  onCheckedChange: toggleAnimateMessages,
8209
8522
  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",
8210
8523
  children: [
8211
- /* @__PURE__ */ jsx40(Zap4, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8524
+ /* @__PURE__ */ jsx40(Zap5, { className: "w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" }),
8212
8525
  /* @__PURE__ */ jsx40("span", { className: "flex-1 font-normal", children: "Simulate Messages" }),
8213
8526
  /* @__PURE__ */ jsx40(
8214
8527
  "div",
@@ -8992,6 +9305,120 @@ function flatLayout(nodes, edges, graphOpts, nodeSize, style) {
8992
9305
  return { nodes: layoutNodes, edges: layoutEdges };
8993
9306
  }
8994
9307
 
9308
+ // src/utils/local-packing.ts
9309
+ var toNumber = (value) => {
9310
+ if (typeof value === "number") return value;
9311
+ if (typeof value === "string") {
9312
+ const parsed = Number.parseFloat(value);
9313
+ return Number.isFinite(parsed) ? parsed : void 0;
9314
+ }
9315
+ return void 0;
9316
+ };
9317
+ var getPackableNodeSize = (node) => ({
9318
+ width: toNumber(node.measured?.width) ?? toNumber(node.width) ?? toNumber(node.style?.width) ?? 260,
9319
+ height: toNumber(node.measured?.height) ?? toNumber(node.height) ?? toNumber(node.style?.height) ?? 140
9320
+ });
9321
+ 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;
9322
+ var toRect = (node, y = node.position.y) => {
9323
+ const size = getPackableNodeSize(node);
9324
+ return {
9325
+ x: node.position.x,
9326
+ y,
9327
+ width: size.width,
9328
+ height: size.height
9329
+ };
9330
+ };
9331
+ var packNodesAroundBounds = ({
9332
+ nodes,
9333
+ movableNodeIds,
9334
+ protectedBounds,
9335
+ groupNodeId,
9336
+ gap = 40
9337
+ }) => {
9338
+ const movableNodes = nodes.filter((node) => movableNodeIds.has(node.id)).sort((a, b) => a.position.y - b.position.y);
9339
+ const movableIds = new Set(movableNodes.map((node) => node.id));
9340
+ const occupiedRects = [
9341
+ protectedBounds,
9342
+ ...nodes.filter(
9343
+ (node) => !node.parentId && node.id !== groupNodeId && !movableIds.has(node.id)
9344
+ ).map((node) => toRect(node))
9345
+ ];
9346
+ const plannedPositions = /* @__PURE__ */ new Map();
9347
+ const groupCenterY = protectedBounds.y + protectedBounds.height / 2;
9348
+ const placeNode = (node) => {
9349
+ const size = getPackableNodeSize(node);
9350
+ const nodeCenterY = node.position.y + size.height / 2;
9351
+ const moveDirection = nodeCenterY >= groupCenterY ? 1 : -1;
9352
+ let y = moveDirection > 0 ? Math.max(
9353
+ node.position.y,
9354
+ protectedBounds.y + protectedBounds.height + gap
9355
+ ) : Math.min(node.position.y, protectedBounds.y - size.height - gap);
9356
+ let attempts = 0;
9357
+ while (attempts < occupiedRects.length + 8) {
9358
+ const rect = {
9359
+ x: node.position.x,
9360
+ y,
9361
+ width: size.width,
9362
+ height: size.height
9363
+ };
9364
+ const collision = occupiedRects.find(
9365
+ (occupied) => rectsIntersect(rect, occupied, gap)
9366
+ );
9367
+ if (!collision) {
9368
+ occupiedRects.push(rect);
9369
+ plannedPositions.set(node.id, { ...node.position, y });
9370
+ return;
9371
+ }
9372
+ y = moveDirection > 0 ? collision.y + collision.height + gap : collision.y - size.height - gap;
9373
+ attempts += 1;
9374
+ }
9375
+ occupiedRects.push({
9376
+ x: node.position.x,
9377
+ y,
9378
+ width: size.width,
9379
+ height: size.height
9380
+ });
9381
+ plannedPositions.set(node.id, { ...node.position, y });
9382
+ };
9383
+ const below = movableNodes.filter((node) => {
9384
+ const size = getPackableNodeSize(node);
9385
+ return node.position.y + size.height / 2 >= groupCenterY;
9386
+ });
9387
+ const belowIds = new Set(below.map((node) => node.id));
9388
+ const above = movableNodes.filter((node) => !belowIds.has(node.id)).reverse();
9389
+ below.forEach(placeNode);
9390
+ above.forEach(placeNode);
9391
+ return plannedPositions;
9392
+ };
9393
+
9394
+ // src/utils/message-group-expansion.ts
9395
+ var getExpandedMessageGroupNode = (nodes, groupNodeId) => nodes.find(
9396
+ (node) => node.id === groupNodeId && node.type === "messageGroupExpanded"
9397
+ );
9398
+ var buildMessageGroupExpansionNodes = ({
9399
+ currentNodes,
9400
+ groupNodeId,
9401
+ expandedContainerNode,
9402
+ childNodes,
9403
+ downstreamNodes,
9404
+ getDownstreamPosition
9405
+ }) => {
9406
+ const withoutExistingGroup = currentNodes.filter(
9407
+ (node) => node.id !== groupNodeId && node.parentId !== groupNodeId
9408
+ );
9409
+ const existingIds = new Set(withoutExistingGroup.map((node) => node.id));
9410
+ const newDownstream = downstreamNodes.filter((node) => !existingIds.has(node.id)).map((node, index) => ({
9411
+ ...node,
9412
+ position: getDownstreamPosition(node, index)
9413
+ }));
9414
+ return [
9415
+ ...withoutExistingGroup,
9416
+ expandedContainerNode,
9417
+ ...childNodes,
9418
+ ...newDownstream
9419
+ ];
9420
+ };
9421
+
8995
9422
  // src/components/NotesToolbarButton.tsx
8996
9423
  import { useState as useState13, useCallback as useCallback11 } from "react";
8997
9424
  import {
@@ -9001,14 +9428,14 @@ import {
9001
9428
  Locate,
9002
9429
  ChevronRight,
9003
9430
  ServerIcon as ServerIcon2,
9004
- Zap as Zap5,
9005
- MessageSquare as MessageSquare2,
9431
+ Zap as Zap6,
9432
+ MessageSquare as MessageSquare3,
9006
9433
  Search as Search3,
9007
9434
  ArrowRightLeft as ArrowRightLeft3,
9008
- Database as Database5,
9435
+ Database as Database6,
9009
9436
  Package as Package2,
9010
9437
  Globe as Globe5,
9011
- User as User3,
9438
+ User as User4,
9012
9439
  MonitorIcon as MonitorIcon2,
9013
9440
  BoxesIcon as BoxesIcon2
9014
9441
  } from "lucide-react";
@@ -9018,18 +9445,18 @@ import { Fragment as Fragment11, jsx as jsx42, jsxs as jsxs40 } from "react/jsx-
9018
9445
  var NODE_TYPE_META = {
9019
9446
  service: { icon: ServerIcon2, color: "#ec4899", label: "Service" },
9020
9447
  services: { icon: ServerIcon2, color: "#ec4899", label: "Service" },
9021
- event: { icon: Zap5, color: "#f97316", label: "Event" },
9022
- events: { icon: Zap5, color: "#f97316", label: "Event" },
9023
- command: { icon: MessageSquare2, color: "#3b82f6", label: "Command" },
9024
- commands: { icon: MessageSquare2, color: "#3b82f6", label: "Command" },
9448
+ event: { icon: Zap6, color: "#f97316", label: "Event" },
9449
+ events: { icon: Zap6, color: "#f97316", label: "Event" },
9450
+ command: { icon: MessageSquare3, color: "#3b82f6", label: "Command" },
9451
+ commands: { icon: MessageSquare3, color: "#3b82f6", label: "Command" },
9025
9452
  query: { icon: Search3, color: "#22c55e", label: "Query" },
9026
9453
  queries: { icon: Search3, color: "#22c55e", label: "Query" },
9027
9454
  channel: { icon: ArrowRightLeft3, color: "#6b7280", label: "Channel" },
9028
9455
  channels: { icon: ArrowRightLeft3, color: "#6b7280", label: "Channel" },
9029
- data: { icon: Database5, color: "#3b82f6", label: "Data" },
9456
+ data: { icon: Database6, color: "#3b82f6", label: "Data" },
9030
9457
  "data-products": { icon: Package2, color: "#6366f1", label: "Data Product" },
9031
9458
  externalSystem: { icon: Globe5, color: "#ec4899", label: "External System" },
9032
- actor: { icon: User3, color: "#eab308", label: "Actor" },
9459
+ actor: { icon: User4, color: "#eab308", label: "Actor" },
9033
9460
  view: { icon: MonitorIcon2, color: "#8b5cf6", label: "View" },
9034
9461
  domain: { icon: BoxesIcon2, color: "#14b8a6", label: "Domain" },
9035
9462
  domains: { icon: BoxesIcon2, color: "#14b8a6", label: "Domain" }
@@ -9931,7 +10358,7 @@ var NodeGraphBuilder = ({
9931
10358
  );
9932
10359
  const [nodes, setNodes, onNodesChange] = useNodesState2(initialNodes);
9933
10360
  const [edges, setEdges, onEdgesChange] = useEdgesState2(initialEdges);
9934
- const { fitView, getNodes } = useReactFlow5();
10361
+ const { fitView, getNodes, getIntersectingNodes, getZoom, setCenter } = useReactFlow5();
9935
10362
  useEffect8(() => {
9936
10363
  setNodes(initialNodes);
9937
10364
  setEdges(initialEdges);
@@ -10071,57 +10498,107 @@ var NodeGraphBuilder = ({
10071
10498
  wrapper.classList.add("ec-animating-layout");
10072
10499
  setTimeout(() => wrapper.classList.remove("ec-animating-layout"), 400);
10073
10500
  }, []);
10074
- const relayoutGraph = useCallback12((nextNodes, nextEdges) => {
10075
- const g = new dagre3.graphlib.Graph({ compound: true });
10076
- g.setGraph({ rankdir: "LR", ranksep: 300, nodesep: 50 });
10077
- g.setDefaultEdgeLabel(() => ({}));
10078
- nextNodes.forEach((node) => {
10079
- if (node.parentId) return;
10080
- const w = node.style?.width || (node.type === "messageGroupExpanded" ? 380 : 150);
10081
- const h = node.style?.height || (node.type === "messageGroupExpanded" ? 0 : 120);
10082
- if (node.type === "messageGroupExpanded") {
10083
- const children = nextNodes.filter((n) => n.parentId === node.id);
10084
- const childHeight = children.length * 190 + 100;
10085
- g.setNode(node.id, { width: w, height: childHeight });
10086
- } else {
10087
- g.setNode(node.id, { width: w, height: h });
10088
- }
10089
- });
10090
- nextEdges.forEach((edge) => {
10091
- const sourceNode = nextNodes.find((n) => n.id === edge.source);
10092
- const targetNode = nextNodes.find((n) => n.id === edge.target);
10093
- const sourceTop = sourceNode?.parentId || edge.source;
10094
- const targetTop = targetNode?.parentId || edge.target;
10095
- if (g.hasNode(sourceTop) && g.hasNode(targetTop) && sourceTop !== targetTop) {
10096
- g.setEdge(sourceTop, targetTop);
10097
- }
10098
- });
10099
- dagre3.layout(g);
10100
- const positioned = nextNodes.map((node) => {
10101
- if (node.parentId) {
10102
- const parent = nextNodes.find((n) => n.id === node.parentId);
10103
- if (parent?.type === "flowExpanded") {
10104
- return node;
10501
+ const relayoutGraph = useCallback12(
10502
+ (nextNodes, nextEdges, anchor) => {
10503
+ const g = new dagre3.graphlib.Graph({ compound: true });
10504
+ g.setGraph({ rankdir: "LR", ranksep: 300, nodesep: 50 });
10505
+ g.setDefaultEdgeLabel(() => ({}));
10506
+ nextNodes.forEach((node) => {
10507
+ if (node.parentId) return;
10508
+ const w = node.style?.width || (node.type === "messageGroupExpanded" ? 380 : 150);
10509
+ const h = node.style?.height || (node.type === "messageGroupExpanded" ? 0 : 120);
10510
+ if (node.type === "messageGroupExpanded") {
10511
+ const children = nextNodes.filter((n) => n.parentId === node.id);
10512
+ const childHeight = children.length * 190 + 100;
10513
+ g.setNode(node.id, { width: w, height: childHeight });
10514
+ } else {
10515
+ g.setNode(node.id, { width: w, height: h });
10516
+ }
10517
+ });
10518
+ nextEdges.forEach((edge) => {
10519
+ const sourceNode = nextNodes.find((n) => n.id === edge.source);
10520
+ const targetNode = nextNodes.find((n) => n.id === edge.target);
10521
+ const sourceTop = sourceNode?.parentId || edge.source;
10522
+ const targetTop = targetNode?.parentId || edge.target;
10523
+ if (g.hasNode(sourceTop) && g.hasNode(targetTop) && sourceTop !== targetTop) {
10524
+ g.setEdge(sourceTop, targetTop);
10105
10525
  }
10106
- const parentWidth = parent?.style?.width || 380;
10107
- const childWidth = 240;
10108
- const xOffset = Math.max(20, (parentWidth - childWidth) / 2);
10109
- const siblings = nextNodes.filter((n) => n.parentId === node.parentId);
10110
- const index = siblings.indexOf(node);
10526
+ });
10527
+ dagre3.layout(g);
10528
+ const positioned = nextNodes.map((node) => {
10529
+ if (node.parentId) {
10530
+ const parent = nextNodes.find((n) => n.id === node.parentId);
10531
+ if (parent?.type === "flowExpanded") {
10532
+ return node;
10533
+ }
10534
+ const parentWidth = parent?.style?.width || 380;
10535
+ const childWidth = 240;
10536
+ const xOffset = Math.max(20, (parentWidth - childWidth) / 2);
10537
+ const siblings = nextNodes.filter(
10538
+ (n) => n.parentId === node.parentId
10539
+ );
10540
+ const index = siblings.indexOf(node);
10541
+ return {
10542
+ ...node,
10543
+ position: { x: xOffset, y: 70 + index * 190 }
10544
+ };
10545
+ }
10546
+ const pos = g.node(node.id);
10547
+ if (!pos) return node;
10111
10548
  return {
10112
10549
  ...node,
10113
- position: { x: xOffset, y: 70 + index * 190 }
10550
+ position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 }
10114
10551
  };
10115
- }
10116
- const pos = g.node(node.id);
10117
- if (!pos) return node;
10118
- return {
10119
- ...node,
10120
- position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 }
10552
+ });
10553
+ if (!anchor) return positioned;
10554
+ const positionedAnchor = positioned.find((node) => node.id === anchor.id);
10555
+ if (!positionedAnchor) return positioned;
10556
+ const offset = {
10557
+ x: anchor.position.x - positionedAnchor.position.x,
10558
+ y: anchor.position.y - positionedAnchor.position.y
10121
10559
  };
10122
- });
10123
- return positioned;
10124
- }, []);
10560
+ return positioned.map((node) => {
10561
+ if (node.parentId) return node;
10562
+ return {
10563
+ ...node,
10564
+ position: {
10565
+ x: node.position.x + offset.x,
10566
+ y: node.position.y + offset.y
10567
+ }
10568
+ };
10569
+ });
10570
+ },
10571
+ []
10572
+ );
10573
+ const makeRoomForRenderedExpandedGroup = useCallback12(
10574
+ (groupNodeId, groupBounds) => {
10575
+ const padding = 80;
10576
+ const protectedBounds = {
10577
+ x: groupBounds.x - padding,
10578
+ y: groupBounds.y - padding,
10579
+ width: groupBounds.width + padding * 2,
10580
+ height: groupBounds.height + padding * 2
10581
+ };
10582
+ const intersectingIds = new Set(
10583
+ getIntersectingNodes(protectedBounds, true).filter((node) => node.id !== groupNodeId && !node.parentId).map((node) => node.id)
10584
+ );
10585
+ if (intersectingIds.size === 0) return;
10586
+ setNodes((currentNodes) => {
10587
+ const plannedPositions = packNodesAroundBounds({
10588
+ nodes: currentNodes,
10589
+ movableNodeIds: intersectingIds,
10590
+ protectedBounds,
10591
+ groupNodeId
10592
+ });
10593
+ return currentNodes.map((node) => {
10594
+ const plannedPosition = plannedPositions.get(node.id);
10595
+ if (!plannedPosition) return node;
10596
+ return { ...node, position: plannedPosition };
10597
+ });
10598
+ });
10599
+ },
10600
+ [getIntersectingNodes, setNodes]
10601
+ );
10125
10602
  const layoutSubFlowChildren = useCallback12(
10126
10603
  (children, edges2, sizeOf, opts) => {
10127
10604
  const { padding, headerH, fallbackW = 240, fallbackH = 120 } = opts;
@@ -10219,9 +10696,19 @@ var NodeGraphBuilder = ({
10219
10696
  }
10220
10697
  const without = prev.filter(
10221
10698
  (n) => n.id !== groupNodeId && !childNodeIds.has(n.id) && !(downstreamNodeIds.has(n.id) && !referencedByEdges.has(n.id))
10222
- );
10223
- const next = [...without, originalNode];
10224
- return relayoutGraph(next, nextEdges);
10699
+ ).map((n) => {
10700
+ const stashedPosition = stashed?.nodePositions?.[n.id];
10701
+ if (!stashedPosition || n.parentId) return n;
10702
+ return { ...n, position: stashedPosition };
10703
+ });
10704
+ const next = [
10705
+ ...without,
10706
+ {
10707
+ ...originalNode,
10708
+ position: stashed?.nodePositions?.[groupNodeId] ?? expandedNode.position
10709
+ }
10710
+ ];
10711
+ return next;
10225
10712
  });
10226
10713
  setEdges((prev) => {
10227
10714
  const without = prev.filter(
@@ -10229,9 +10716,6 @@ var NodeGraphBuilder = ({
10229
10716
  );
10230
10717
  return [...without, ...originalEdges];
10231
10718
  });
10232
- requestAnimationFrame(() => {
10233
- fitView({ duration: 400, padding: 0.2 });
10234
- });
10235
10719
  },
10236
10720
  [
10237
10721
  initialNodes,
@@ -10239,8 +10723,7 @@ var NodeGraphBuilder = ({
10239
10723
  setNodes,
10240
10724
  setEdges,
10241
10725
  relayoutGraph,
10242
- animateLayout,
10243
- fitView
10726
+ animateLayout
10244
10727
  ]
10245
10728
  );
10246
10729
  useEffect8(() => {
@@ -10323,9 +10806,6 @@ var NodeGraphBuilder = ({
10323
10806
  onNodeClick(node);
10324
10807
  return;
10325
10808
  }
10326
- if (linksToVisualiser && onNavigate) {
10327
- return;
10328
- }
10329
10809
  const isFlow = edgesRef.current.some(
10330
10810
  (edge) => edge.type === "flow-edge"
10331
10811
  );
@@ -10339,6 +10819,24 @@ var NodeGraphBuilder = ({
10339
10819
  if (node.type === "messageGroup") {
10340
10820
  const groupData = node.data;
10341
10821
  const groupNodeId = node.id;
10822
+ const currentGroupNode = getExpandedMessageGroupNode(
10823
+ nodesRef.current,
10824
+ groupNodeId
10825
+ );
10826
+ if (currentGroupNode?.type === "messageGroupExpanded") {
10827
+ const measured = currentGroupNode?.measured;
10828
+ const width = measured?.width ?? currentGroupNode.style?.width ?? 380;
10829
+ const height = measured?.height ?? currentGroupNode.style?.height ?? 300;
10830
+ setCenter(
10831
+ currentGroupNode.position.x + width / 2,
10832
+ currentGroupNode.position.y + height / 2,
10833
+ {
10834
+ duration: 300,
10835
+ zoom: Math.min(Math.max(getZoom(), 0.55), 1)
10836
+ }
10837
+ );
10838
+ return;
10839
+ }
10342
10840
  const serviceNodeId = `${groupData.service.id}-${groupData.service.version}`;
10343
10841
  const childCount = groupData.messages?.length || 0;
10344
10842
  const containerWidth = 380;
@@ -10347,6 +10845,9 @@ var NodeGraphBuilder = ({
10347
10845
  (e) => e.source === groupNodeId || e.target === groupNodeId
10348
10846
  );
10349
10847
  const preExpansionNodeIds = nodesRef.current.map((n) => n.id);
10848
+ const preExpansionNodePositions = Object.fromEntries(
10849
+ nodesRef.current.map((n) => [n.id, { ...n.position }])
10850
+ );
10350
10851
  const expandedContainerNode = {
10351
10852
  id: groupNodeId,
10352
10853
  type: "messageGroupExpanded",
@@ -10359,7 +10860,8 @@ var NodeGraphBuilder = ({
10359
10860
  __preExpansion: {
10360
10861
  node,
10361
10862
  edges: preExpansionEdges,
10362
- nodeIds: preExpansionNodeIds
10863
+ nodeIds: preExpansionNodeIds,
10864
+ nodePositions: preExpansionNodePositions
10363
10865
  }
10364
10866
  },
10365
10867
  style: {
@@ -10420,24 +10922,19 @@ var NodeGraphBuilder = ({
10420
10922
  });
10421
10923
  animateLayout();
10422
10924
  setNodes((prev) => {
10423
- const without = prev.filter((n) => n.id !== groupNodeId);
10424
- const existingIds = new Set(without.map((n) => n.id));
10425
- const newDownstream = downstreamNodes.filter(
10426
- (n) => !existingIds.has(n.id)
10427
- );
10428
- const next = [
10429
- ...without,
10925
+ const downstreamX = groupData.direction === "sends" ? node.position.x + containerWidth + 260 : node.position.x - 420;
10926
+ const downstreamY = node.position.y + 40;
10927
+ return buildMessageGroupExpansionNodes({
10928
+ currentNodes: prev,
10929
+ groupNodeId,
10430
10930
  expandedContainerNode,
10431
- ...childNodes,
10432
- ...newDownstream
10433
- ];
10434
- return relayoutGraph(next, [
10435
- ...edgesRef.current.filter(
10436
- (e) => e.source !== groupNodeId && e.target !== groupNodeId
10437
- ),
10438
- ...childEdges,
10439
- ...downstreamEdges
10440
- ]);
10931
+ childNodes,
10932
+ downstreamNodes,
10933
+ getDownstreamPosition: (_downstreamNode, index) => ({
10934
+ x: downstreamX,
10935
+ y: downstreamY + index * 190
10936
+ })
10937
+ });
10441
10938
  });
10442
10939
  setEdges((prev) => {
10443
10940
  const without = prev.filter(
@@ -10446,6 +10943,12 @@ var NodeGraphBuilder = ({
10446
10943
  return [...without, ...childEdges, ...downstreamEdges];
10447
10944
  });
10448
10945
  requestAnimationFrame(() => {
10946
+ let actualContainerBounds = {
10947
+ x: node.position.x,
10948
+ y: node.position.y,
10949
+ width: containerWidth,
10950
+ height: containerHeight
10951
+ };
10449
10952
  setNodes((prev) => {
10450
10953
  const children = prev.filter((n) => n.parentId === groupNodeId);
10451
10954
  if (children.length === 0) return prev;
@@ -10465,8 +10968,20 @@ var NodeGraphBuilder = ({
10465
10968
  const totalChildH = measurements.reduce((sum, m) => sum + m.h, 0) + gap * (measurements.length - 1);
10466
10969
  const actualContainerH = headerH + totalChildH + padding * 2;
10467
10970
  let currentY = headerH + padding;
10971
+ actualContainerBounds = {
10972
+ x: node.position.x,
10973
+ y: node.position.y,
10974
+ width: containerWidth,
10975
+ height: actualContainerH
10976
+ };
10468
10977
  return prev.map((n) => {
10469
10978
  if (n.id === groupNodeId) {
10979
+ actualContainerBounds = {
10980
+ x: n.position.x,
10981
+ y: n.position.y,
10982
+ width: containerWidth,
10983
+ height: actualContainerH
10984
+ };
10470
10985
  return {
10471
10986
  ...n,
10472
10987
  style: {
@@ -10484,9 +10999,23 @@ var NodeGraphBuilder = ({
10484
10999
  return { ...n, position: { x, y } };
10485
11000
  });
10486
11001
  });
10487
- });
10488
- requestAnimationFrame(() => {
10489
- fitView({ duration: 400, padding: 0.2 });
11002
+ requestAnimationFrame(() => {
11003
+ const groupNode = getNodes().find((n) => n.id === groupNodeId);
11004
+ const measured = groupNode?.measured;
11005
+ const width = measured?.width ?? groupNode?.style?.width ?? containerWidth;
11006
+ const height = measured?.height ?? groupNode?.style?.height ?? actualContainerBounds.height;
11007
+ const bounds = {
11008
+ x: groupNode?.position.x ?? actualContainerBounds.x,
11009
+ y: groupNode?.position.y ?? actualContainerBounds.y,
11010
+ width,
11011
+ height
11012
+ };
11013
+ makeRoomForRenderedExpandedGroup(groupNodeId, bounds);
11014
+ setCenter(bounds.x + width / 2, bounds.y + height / 2, {
11015
+ duration: 450,
11016
+ zoom: Math.min(Math.max(getZoom(), 0.55), 1)
11017
+ });
11018
+ });
10490
11019
  });
10491
11020
  return;
10492
11021
  }
@@ -10651,10 +11180,21 @@ var NodeGraphBuilder = ({
10651
11180
  });
10652
11181
  return;
10653
11182
  }
11183
+ if (linksToVisualiser && onNavigate) {
11184
+ return;
11185
+ }
10654
11186
  setFocusedNodeId(node.id);
10655
11187
  setFocusModeOpen(true);
10656
11188
  },
10657
- [onNodeClick, linksToVisualiser, onNavigate]
11189
+ [
11190
+ onNodeClick,
11191
+ linksToVisualiser,
11192
+ onNavigate,
11193
+ makeRoomForRenderedExpandedGroup,
11194
+ getNodes,
11195
+ getZoom,
11196
+ setCenter
11197
+ ]
10658
11198
  );
10659
11199
  const toggleAnimateMessages = useCallback12(() => {
10660
11200
  setAnimateMessages((prev) => {
@@ -10884,13 +11424,15 @@ var NodeGraphBuilder = ({
10884
11424
  }, [getNodes, downloadImage, title]);
10885
11425
  const handleLegendClick = useCallback12(
10886
11426
  (collectionType, groupId) => {
11427
+ const isLegendTarget = (node) => {
11428
+ if (groupId) {
11429
+ return node.data.group && node.data.group?.id === groupId;
11430
+ }
11431
+ return node.type === collectionType;
11432
+ };
10887
11433
  const updatedNodes = nodes.map((node) => {
10888
- if (groupId && node.data.group && node.data.group?.id === groupId) {
11434
+ if (isLegendTarget(node)) {
10889
11435
  return { ...node, style: { ...node.style, opacity: 1 } };
10890
- } else {
10891
- if (node.type === collectionType) {
10892
- return { ...node, style: { ...node.style, opacity: 1 } };
10893
- }
10894
11436
  }
10895
11437
  return { ...node, style: { ...node.style, opacity: 0.1 } };
10896
11438
  });
@@ -10905,10 +11447,12 @@ var NodeGraphBuilder = ({
10905
11447
  });
10906
11448
  setNodes(updatedNodes);
10907
11449
  setEdges(updatedEdges);
11450
+ const targetNodes = updatedNodes.filter(isLegendTarget);
11451
+ if (targetNodes.length === 0) return;
10908
11452
  fitView({
10909
11453
  padding: 0.2,
10910
11454
  duration: 800,
10911
- nodes: updatedNodes.filter((node) => node.type === collectionType)
11455
+ nodes: targetNodes
10912
11456
  });
10913
11457
  },
10914
11458
  [nodes, edges, setNodes, setEdges, fitView]