@eventcatalog/core 5.0.0-beta.0 → 5.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +227 -128
  2. package/dist/analytics/analytics.cjs +19 -3
  3. package/dist/analytics/analytics.d.cts +1 -1
  4. package/dist/analytics/analytics.d.ts +1 -1
  5. package/dist/analytics/analytics.js +3 -2
  6. package/dist/analytics/log-build.cjs +72 -35
  7. package/dist/analytics/log-build.js +5 -4
  8. package/dist/analytics/telemetry.cjs +42 -0
  9. package/dist/analytics/telemetry.d.cts +20 -0
  10. package/dist/analytics/telemetry.d.ts +20 -0
  11. package/dist/analytics/telemetry.js +8 -0
  12. package/dist/{chunk-ZHKEARLJ.js → chunk-2LBDRHE4.js} +1 -1
  13. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  14. package/dist/chunk-CPW6G62V.js +17 -0
  15. package/dist/{chunk-LR7AGRHN.js → chunk-CRKJBDN7.js} +56 -33
  16. package/dist/{chunk-CR6AK5LR.js → chunk-EM36OL6A.js} +1 -1
  17. package/dist/{chunk-TWN43V4T.js → chunk-IONYKKOY.js} +1 -1
  18. package/dist/{chunk-WAIADKQE.js → chunk-LJMXOEEC.js} +10 -3
  19. package/dist/constants.cjs +1 -1
  20. package/dist/constants.js +1 -1
  21. package/dist/eventcatalog.cjs +205 -124
  22. package/dist/eventcatalog.config.d.cts +59 -1
  23. package/dist/eventcatalog.config.d.ts +59 -1
  24. package/dist/eventcatalog.js +30 -11
  25. package/dist/federation/federate.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  29. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  30. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  31. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  32. package/dist/runtime-dependencies/manifest.json +8 -1
  33. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  34. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  35. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  36. package/dist/runtime-dependencies/yjs.mjs +3 -0
  37. package/dist/utils/cli-logger.cjs +1 -1
  38. package/dist/utils/cli-logger.js +2 -2
  39. package/dist/utils/license-status.cjs +71 -45
  40. package/dist/utils/license-status.d.cts +14 -5
  41. package/dist/utils/license-status.d.ts +14 -5
  42. package/dist/utils/license-status.js +1 -1
  43. package/eventcatalog/astro.config.mjs +5 -0
  44. package/eventcatalog/integrations/storage.mjs +20 -0
  45. package/eventcatalog/integrations/studio-server.mjs +32 -0
  46. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  47. package/eventcatalog/src/components/Header.astro +3 -3
  48. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  49. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  50. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  51. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  52. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  53. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  54. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  55. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  56. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  57. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  58. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  59. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  60. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  61. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  62. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
  63. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
  64. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  65. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  66. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  67. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  68. package/eventcatalog/src/features/server/start.ts +25 -0
  69. package/eventcatalog/src/features/storage/database.ts +88 -0
  70. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  71. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  72. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  73. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  74. package/eventcatalog/src/features/storage/sql.ts +28 -0
  75. package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
  76. package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
  77. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  78. package/eventcatalog/src/features/studio/api/client.ts +35 -0
  79. package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
  80. package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
  81. package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
  82. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  83. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  84. package/eventcatalog/src/features/studio/clipboard.ts +97 -0
  85. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
  86. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
  87. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
  88. package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
  89. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
  90. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
  91. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  92. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
  93. package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
  94. package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
  95. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
  96. package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
  97. package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
  98. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
  99. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
  100. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
  101. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  102. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
  103. package/eventcatalog/src/features/studio/components/icons.ts +53 -0
  104. package/eventcatalog/src/features/studio/components/status.ts +53 -0
  105. package/eventcatalog/src/features/studio/edges.ts +56 -0
  106. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  107. package/eventcatalog/src/features/studio/grouping.ts +226 -0
  108. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  109. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  110. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
  111. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  112. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
  113. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
  114. package/eventcatalog/src/features/studio/identity.ts +37 -0
  115. package/eventcatalog/src/features/studio/layout.ts +42 -0
  116. package/eventcatalog/src/features/studio/levels.ts +282 -0
  117. package/eventcatalog/src/features/studio/node-types.ts +205 -0
  118. package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
  119. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  120. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  121. package/eventcatalog/src/features/studio/placement.ts +197 -0
  122. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
  123. package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
  124. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  125. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  126. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  127. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  128. package/eventcatalog/src/features/studio/studio.css +273 -0
  129. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  130. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
  131. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  132. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  133. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  134. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  135. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  136. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  137. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  138. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  139. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  140. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  141. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  142. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  143. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  144. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  145. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  146. package/eventcatalog/src/styles/tailwind.css +2 -2
  147. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  148. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  149. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  150. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  151. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  152. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  153. package/eventcatalog/src/utils/collections/util.ts +31 -0
  154. package/eventcatalog/src/utils/feature.ts +8 -0
  155. package/eventcatalog/src/utils/git.ts +11 -5
  156. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  157. package/eventcatalog/src/utils/trial.ts +1 -1
  158. package/package.json +17 -12
  159. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  160. package/eventcatalog/public/studio-bg.png +0 -0
  161. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  162. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  163. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  164. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  165. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  166. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  167. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  168. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  169. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  170. package/eventcatalog/src/pages/studio.astro +0 -316
  171. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -0,0 +1,226 @@
1
+ import type { Node, XYPosition } from '@xyflow/react';
2
+ import { canGoInContainer, getNodeSize, isGroupType } from './node-types';
3
+
4
+ /**
5
+ * Containers: domains and systems shown as boxes that other nodes sit in.
6
+ * A node inside one has its parentId, and its position is relative to the container.
7
+ */
8
+
9
+ /** Room around what's in a container, and for its header */
10
+ export const GROUP_PADDING = { top: 80, right: 40, bottom: 40, left: 40 };
11
+
12
+ type Rect = { x: number; y: number; width: number; height: number };
13
+
14
+ const byId = (nodes: Node[]) => new Map(nodes.map((node) => [node.id, node]));
15
+
16
+ /** A node's size: as rendered (in the browser), as set (containers, notes), or what its type renders at */
17
+ export const sizeOf = (node: Node) => ({
18
+ width: node.measured?.width ?? node.width ?? getNodeSize(node.type).width,
19
+ height: node.measured?.height ?? node.height ?? getNodeSize(node.type).height,
20
+ });
21
+
22
+ /** Nodes are positioned relative to their container, so walk up the chain to get the canvas position */
23
+ export const getAbsolutePosition = (node: Node, lookup: Map<string, Node>): XYPosition => {
24
+ const parent = node.parentId ? lookup.get(node.parentId) : undefined;
25
+ if (!parent) return node.position;
26
+ const parentPosition = getAbsolutePosition(parent, lookup);
27
+ return { x: parentPosition.x + node.position.x, y: parentPosition.y + node.position.y };
28
+ };
29
+
30
+ export const getAbsoluteRect = (node: Node, lookup: Map<string, Node>): Rect => ({
31
+ ...getAbsolutePosition(node, lookup),
32
+ ...sizeOf(node),
33
+ });
34
+
35
+ const getDepth = (node: Node, lookup: Map<string, Node>): number => {
36
+ const parent = node.parentId ? lookup.get(node.parentId) : undefined;
37
+ return parent ? 1 + getDepth(parent, lookup) : 0;
38
+ };
39
+
40
+ const isDescendantOf = (node: Node, ancestorId: string, lookup: Map<string, Node>): boolean => {
41
+ if (!node.parentId) return false;
42
+ if (node.parentId === ancestorId) return true;
43
+ const parent = lookup.get(node.parentId);
44
+ return parent ? isDescendantOf(parent, ancestorId, lookup) : false;
45
+ };
46
+
47
+ const containsPoint = (rect: Rect, point: XYPosition) =>
48
+ point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
49
+
50
+ /** React Flow needs containers before what's in them in the nodes array */
51
+ export const sortByHierarchy = (nodes: Node[]): Node[] => {
52
+ const lookup = byId(nodes);
53
+ return nodes
54
+ .map((node, index) => ({ node, index, depth: getDepth(node, lookup) }))
55
+ .sort((a, b) => a.depth - b.depth || a.index - b.index)
56
+ .map(({ node }) => node);
57
+ };
58
+
59
+ /** The innermost container under the centre of a node (never itself, or one of its own children) */
60
+ export const findDropTarget = (nodeId: string, nodes: Node[]): Node | undefined => {
61
+ const lookup = byId(nodes);
62
+ const node = lookup.get(nodeId);
63
+ // Sticky notes go anywhere, never in a container (one that's in one is taken out when it's next moved)
64
+ if (!node || !canGoInContainer(node.type)) return undefined;
65
+ const rect = getAbsoluteRect(node, lookup);
66
+ const center = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
67
+ return nodes
68
+ .filter((candidate) => isGroupType(candidate.type) && candidate.id !== nodeId && !isDescendantOf(candidate, nodeId, lookup))
69
+ .filter((candidate) => containsPoint(getAbsoluteRect(candidate, lookup), center))
70
+ .sort((a, b) => getDepth(b, lookup) - getDepth(a, lookup))[0];
71
+ };
72
+
73
+ /** The innermost container at a canvas point, for things dropped there */
74
+ export const findGroupAtPoint = (point: XYPosition, nodes: Node[]): Node | undefined => {
75
+ const lookup = byId(nodes);
76
+ return nodes
77
+ .filter((candidate) => isGroupType(candidate.type) && containsPoint(getAbsoluteRect(candidate, lookup), point))
78
+ .sort((a, b) => getDepth(b, lookup) - getDepth(a, lookup))[0];
79
+ };
80
+
81
+ /** A node moved into (or out of, without a parentId) a container, staying in the same place on the canvas */
82
+ export const withParent = (node: Node, parentId: string | undefined, nodes: Node[]): Node => {
83
+ const lookup = byId(nodes);
84
+ const absolute = getAbsolutePosition(node, lookup);
85
+ const parent = parentId ? lookup.get(parentId) : undefined;
86
+ const parentPosition = parent ? getAbsolutePosition(parent, lookup) : { x: 0, y: 0 };
87
+ const { parentId: _, extent: __, ...rest } = node;
88
+ return {
89
+ ...rest,
90
+ ...(parent && { parentId: parent.id }),
91
+ position: { x: absolute.x - parentPosition.x, y: absolute.y - parentPosition.y },
92
+ };
93
+ };
94
+
95
+ /** A canvas point, relative to a container */
96
+ export const toRelativePosition = (point: XYPosition, parentId: string, nodes: Node[]): XYPosition => {
97
+ const lookup = byId(nodes);
98
+ const parent = lookup.get(parentId);
99
+ if (!parent) return point;
100
+ const parentPosition = getAbsolutePosition(parent, lookup);
101
+ return { x: point.x - parentPosition.x, y: point.y - parentPosition.y };
102
+ };
103
+
104
+ /** Nodes and everything inside them (deleted with their container) */
105
+ export const withDescendants = (ids: string[], nodes: Node[]): Set<string> => {
106
+ const lookup = byId(nodes);
107
+ const result = new Set(ids);
108
+ nodes.forEach((node) => {
109
+ if (ids.some((id) => isDescendantOf(node, id, lookup))) result.add(node.id);
110
+ });
111
+ return result;
112
+ };
113
+
114
+ /**
115
+ * The size a container needs to fit what's inside it, never smaller than it is, and how far its children
116
+ * must shift when some sit above or left of its content area (so the header stays clear).
117
+ */
118
+ export const fitGroup = (groupId: string, nodes: Node[]) => {
119
+ const lookup = byId(nodes);
120
+ const group = lookup.get(groupId);
121
+ const children = nodes.filter((node) => node.parentId === groupId);
122
+ if (!group || children.length === 0) return undefined;
123
+ const rects = children.map((child) => ({ ...child.position, ...sizeOf(child) }));
124
+ const shift = {
125
+ x: Math.max(0, GROUP_PADDING.left - Math.min(...rects.map((rect) => rect.x))),
126
+ y: Math.max(0, GROUP_PADDING.top - Math.min(...rects.map((rect) => rect.y))),
127
+ };
128
+ const current = sizeOf(group);
129
+ return {
130
+ shift,
131
+ width: Math.max(current.width, Math.max(...rects.map((rect) => rect.x + rect.width)) + shift.x + GROUP_PADDING.right),
132
+ height: Math.max(current.height, Math.max(...rects.map((rect) => rect.y + rect.height)) + shift.y + GROUP_PADDING.bottom),
133
+ };
134
+ };
135
+
136
+ /** How far a dragged node's middle can go past a container's edge and still be kept in it (growing it) */
137
+ export const KEEP_IN_CONTAINER_MARGIN = 160;
138
+
139
+ type Size = { width: number; height: number };
140
+ /** A node's place and size as shown while something's dragged */
141
+ export type NodePreview = { position: XYPosition; width?: number; height?: number };
142
+
143
+ /**
144
+ * Containers growing to fit what's being dragged in them, on every side, as a preview of where things will be.
145
+ * `containerIds` are the containers around what's dragged, innermost first. `start` is the canvas when the drag
146
+ * started, and `now` the canvas as shown (with the dragged nodes where they are now). A dragged node is kept in a
147
+ * container while its middle is within `KEEP_IN_CONTAINER_MARGIN` of where the container was, and dropping it
148
+ * further away takes it out, so the container goes back to its size. A container growing up or left moves, and
149
+ * what's in it moves the other way, so nothing moves on the canvas.
150
+ *
151
+ * Returns, by id, where containers and the nodes in them that aren't dragged go (positions relative to their
152
+ * containers), and their sizes for containers.
153
+ */
154
+ export const previewContainerGrowth = (
155
+ containerIds: string[],
156
+ draggedIds: ReadonlySet<string>,
157
+ start: ReadonlyMap<string, Node>,
158
+ now: ReadonlyMap<string, Node>
159
+ ): Map<string, NodePreview> => {
160
+ const previews = new Map<string, NodePreview>();
161
+ // Where each container's top left was on the canvas when the drag started (moving a container grown up or
162
+ // left is offset by what's in it moving the other way, so only its own shift changes it)
163
+ const startOrigin = (id: string) => getAbsolutePosition(start.get(id)!, start as Map<string, Node>);
164
+ const shifts = new Map<string, XYPosition>();
165
+
166
+ for (const id of containerIds) {
167
+ const container = start.get(id);
168
+ if (!container) continue;
169
+ const origin = startOrigin(id);
170
+ const startSize = sizeOf(container);
171
+ // Everything in it, in its coordinates when the drag started
172
+ const items: { rect: Rect }[] = [];
173
+ for (const node of start.values()) {
174
+ if (node.parentId !== id) continue;
175
+ if (draggedIds.has(node.id)) {
176
+ const current = now.get(node.id);
177
+ if (!current) continue;
178
+ const at = getAbsoluteRect(current, now as Map<string, Node>);
179
+ const middle = { x: at.x + at.width / 2, y: at.y + at.height / 2 };
180
+ const kept =
181
+ middle.x >= origin.x - KEEP_IN_CONTAINER_MARGIN &&
182
+ middle.x <= origin.x + startSize.width + KEEP_IN_CONTAINER_MARGIN &&
183
+ middle.y >= origin.y - KEEP_IN_CONTAINER_MARGIN &&
184
+ middle.y <= origin.y + startSize.height + KEEP_IN_CONTAINER_MARGIN;
185
+ if (kept) items.push({ rect: { ...at, x: at.x - origin.x, y: at.y - origin.y } });
186
+ continue;
187
+ }
188
+ // A container inside it that's grown: where it's grown to
189
+ const inner = previews.get(node.id);
190
+ const innerShift = shifts.get(node.id) ?? { x: 0, y: 0 };
191
+ items.push({
192
+ rect: {
193
+ x: node.position.x - innerShift.x,
194
+ y: node.position.y - innerShift.y,
195
+ ...(inner?.width && inner.height ? { width: inner.width, height: inner.height } : sizeOf(node)),
196
+ },
197
+ });
198
+ }
199
+
200
+ const shift = { x: 0, y: 0 };
201
+ let width = startSize.width;
202
+ let height = startSize.height;
203
+ if (items.length > 0) {
204
+ shift.x = Math.max(0, GROUP_PADDING.left - Math.min(...items.map(({ rect }) => rect.x)));
205
+ shift.y = Math.max(0, GROUP_PADDING.top - Math.min(...items.map(({ rect }) => rect.y)));
206
+ width =
207
+ Math.max(startSize.width, Math.max(...items.map(({ rect }) => rect.x + rect.width)) + GROUP_PADDING.right) + shift.x;
208
+ height =
209
+ Math.max(startSize.height, Math.max(...items.map(({ rect }) => rect.y + rect.height)) + GROUP_PADDING.bottom) + shift.y;
210
+ }
211
+ shifts.set(id, shift);
212
+ previews.set(id, {
213
+ position: { x: container.position.x - shift.x, y: container.position.y - shift.y },
214
+ width,
215
+ height,
216
+ });
217
+ // What's in it moves the other way. Not what's dragged: React Flow places it against the container as it is.
218
+ for (const node of start.values()) {
219
+ if (node.parentId !== id || draggedIds.has(node.id)) continue;
220
+ const own = previews.get(node.id);
221
+ const base = own?.position ?? node.position;
222
+ previews.set(node.id, { ...own, position: { x: base.x + shift.x, y: base.y + shift.y } });
223
+ }
224
+ }
225
+ return previews;
226
+ };
@@ -0,0 +1,46 @@
1
+ import type { Awareness } from 'y-protocols/awareness';
2
+
3
+ /** About 30 times a second, like Excalidraw and tldraw: smooth for others, without a message on every mousemove */
4
+ export const PRESENCE_INTERVAL_MS = 33;
5
+
6
+ export type PresenceSender = {
7
+ /** Share these fields of our presence: straight away if nothing was sent lately, otherwise with the next send */
8
+ send: (fields: Record<string, unknown>) => void;
9
+ /** Send anything waiting now (e.g. a node dropped, so the drop isn't behind the drag) */
10
+ flush: () => void;
11
+ destroy: () => void;
12
+ };
13
+
14
+ /**
15
+ * Our presence (pointer, nodes being dragged, selection, view), sent to everyone at most every interval. Awareness
16
+ * sends the whole state on every change, so fields changed together (the pointer and the node it drags) go in one
17
+ * message, and the latest value always goes out (the pointer stops where it really stopped).
18
+ */
19
+ export function createPresenceSender(awareness: Awareness, intervalMs = PRESENCE_INTERVAL_MS): PresenceSender {
20
+ let pending: Record<string, unknown> | undefined;
21
+ let timer: ReturnType<typeof setTimeout> | undefined;
22
+ let lastSent = -Infinity;
23
+
24
+ const flush = () => {
25
+ clearTimeout(timer);
26
+ timer = undefined;
27
+ if (!pending) return;
28
+ const fields = pending;
29
+ pending = undefined;
30
+ lastSent = performance.now();
31
+ awareness.setLocalState({ ...awareness.getLocalState(), ...fields });
32
+ };
33
+
34
+ return {
35
+ send: (fields) => {
36
+ pending = { ...pending, ...fields };
37
+ if (timer) return;
38
+ timer = setTimeout(flush, Math.max(0, intervalMs - (performance.now() - lastSent)));
39
+ },
40
+ flush,
41
+ destroy: () => {
42
+ clearTimeout(timer);
43
+ pending = undefined;
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,119 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import type { XYPosition } from '@xyflow/react';
3
+ import type { Awareness } from 'y-protocols/awareness';
4
+
5
+ export type Peer = {
6
+ clientId: number;
7
+ name: string;
8
+ color: string;
9
+ /** A picture from their sign-in provider (SSO), shown over their initials */
10
+ picture?: string;
11
+ /** An AI agent (joined over MCP, or working through this tab with WebMCP), rather than a person */
12
+ agent?: boolean;
13
+ /** What an agent is doing, e.g. "Adding OrderService" */
14
+ activity?: string;
15
+ pointer: XYPosition | null;
16
+ selection: string[];
17
+ /** What they're looking at: the canvas position in the middle of their screen, and their zoom */
18
+ viewport?: { x: number; y: number; zoom: number };
19
+ /** Where a connection they're dragging starts (agents show theirs) */
20
+ connecting?: XYPosition | null;
21
+ };
22
+
23
+ type Listener = () => void;
24
+
25
+ /**
26
+ * Who's on a canvas and what they're doing (pointers, selections, views), from Yjs awareness, kept out of React
27
+ * state: only the components that draw presence subscribe, so a pointer moving doesn't re-render the canvas.
28
+ * Our own pointer, selection and view are sent to others but don't notify here (nothing here draws them).
29
+ */
30
+ export type PresenceStore = {
31
+ subscribe: (listener: Listener) => () => void;
32
+ /** Everyone, including us and an agent working through this tab. Changes whenever anyone does anything. */
33
+ getPeers: () => Peer[];
34
+ /**
35
+ * Everyone, for lists of who's here: changes only when people join, leave or rename, or agents change what
36
+ * they're doing (pointers and views in it may be out of date: read getPeers() for those)
37
+ */
38
+ getPeople: () => Peer[];
39
+ /** Show an agent working through this tab (WebMCP), which only this tab sees */
40
+ setLocalAgent: (agent: Peer | null) => void;
41
+ destroy: () => void;
42
+ };
43
+
44
+ const EMPTY: Peer[] = [];
45
+
46
+ const toPeers = (awareness: Awareness): Peer[] => {
47
+ const peers: Peer[] = [];
48
+ awareness.getStates().forEach((state, clientId) => {
49
+ if (!state.user) return;
50
+ peers.push({
51
+ clientId,
52
+ ...state.user,
53
+ activity: state.activity,
54
+ pointer: state.pointer ?? null,
55
+ selection: state.selection ?? [],
56
+ viewport: state.viewport,
57
+ connecting: state.connecting,
58
+ });
59
+ });
60
+ return peers;
61
+ };
62
+
63
+ // What getPeople() changes on: who people are, what agents are doing, and whether they're on the canvas (a pointer)
64
+ const identity = (peer: Peer) =>
65
+ [peer.clientId, peer.name, peer.color, peer.picture ?? '', peer.agent ? 1 : 0, peer.activity ?? '', peer.pointer ? 1 : 0].join(
66
+ '|'
67
+ );
68
+
69
+ export function createPresenceStore(awareness: Awareness): PresenceStore {
70
+ const listeners = new Set<Listener>();
71
+ let remote = toPeers(awareness);
72
+ let localAgent: Peer | null = null;
73
+ let peers = remote;
74
+ let people = remote;
75
+ let ownUser = JSON.stringify(awareness.getLocalState()?.user ?? null);
76
+
77
+ const update = () => {
78
+ peers = localAgent ? [...remote, localAgent] : remote;
79
+ const nextPeople = peers.length ? peers : EMPTY;
80
+ if (nextPeople.map(identity).join() !== people.map(identity).join()) people = nextPeople;
81
+ listeners.forEach((listener) => listener());
82
+ };
83
+
84
+ const onChange = ({ added, updated, removed }: { added: number[]; updated: number[]; removed: number[] }) => {
85
+ const others = [...added, ...updated, ...removed].some((id) => id !== awareness.clientID);
86
+ const user = JSON.stringify(awareness.getLocalState()?.user ?? null);
87
+ // Only our own pointer, selection or view changed: nothing shown here changed
88
+ if (!others && user === ownUser) return;
89
+ ownUser = user;
90
+ remote = toPeers(awareness);
91
+ update();
92
+ };
93
+ awareness.on('change', onChange);
94
+
95
+ return {
96
+ subscribe: (listener) => {
97
+ listeners.add(listener);
98
+ return () => listeners.delete(listener);
99
+ },
100
+ getPeers: () => peers,
101
+ getPeople: () => people,
102
+ setLocalAgent: (agent) => {
103
+ localAgent = agent;
104
+ update();
105
+ },
106
+ destroy: () => {
107
+ awareness.off('change', onChange);
108
+ listeners.clear();
109
+ },
110
+ };
111
+ }
112
+
113
+ const noStore = { subscribe: () => () => {}, getPeers: () => EMPTY, getPeople: () => EMPTY };
114
+
115
+ /** Everyone on the canvas, re-rendering only when people join, leave, rename or start or stop working */
116
+ export const usePeople = (store: PresenceStore | null) => {
117
+ const source = store ?? noStore;
118
+ return useSyncExternalStore(source.subscribe, source.getPeople);
119
+ };