@eventcatalog/core 5.0.0-beta.3 → 5.0.0-beta.31

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 (277) hide show
  1. package/LICENSE +234 -134
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +160 -77
  8. package/dist/analytics/log-build.d.cts +2 -2
  9. package/dist/analytics/log-build.d.ts +2 -2
  10. package/dist/analytics/log-build.js +7 -5
  11. package/dist/analytics/studio-usage.cjs +90 -0
  12. package/dist/analytics/studio-usage.d.cts +5 -0
  13. package/dist/analytics/studio-usage.d.ts +5 -0
  14. package/dist/analytics/studio-usage.js +10 -0
  15. package/dist/analytics/telemetry.cjs +42 -0
  16. package/dist/analytics/telemetry.d.cts +20 -0
  17. package/dist/analytics/telemetry.d.ts +20 -0
  18. package/dist/analytics/telemetry.js +8 -0
  19. package/dist/catalog-runtime.cjs +1 -1
  20. package/dist/{chunk-BH2F5WPN.js → chunk-2UCCTEIH.js} +1 -1
  21. package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
  22. package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
  23. package/dist/{chunk-NDJOAITI.js → chunk-BNSLGNQI.js} +9 -3
  24. package/dist/chunk-CA6WCQ2T.js +54 -0
  25. package/dist/chunk-CPW6G62V.js +17 -0
  26. package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
  27. package/dist/{chunk-423S2KHN.js → chunk-OUEGYMBA.js} +4 -2
  28. package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
  29. package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
  30. package/dist/chunk-S5GWAUG5.js +29 -0
  31. package/dist/{chunk-GYIVJXOJ.js → chunk-WADZVNCA.js} +1 -1
  32. package/dist/{chunk-6OC46WQJ.js → chunk-XRFL642Z.js} +66 -35
  33. package/dist/constants.cjs +1 -1
  34. package/dist/constants.js +1 -1
  35. package/dist/eventcatalog-config-file-utils.cjs +34 -41
  36. package/dist/eventcatalog-config-file-utils.d.cts +56 -58
  37. package/dist/eventcatalog-config-file-utils.d.ts +56 -58
  38. package/dist/eventcatalog-config-file-utils.js +1 -1
  39. package/dist/eventcatalog.cjs +531 -356
  40. package/dist/eventcatalog.config.d.cts +59 -1
  41. package/dist/eventcatalog.config.d.ts +59 -1
  42. package/dist/eventcatalog.js +63 -25
  43. package/dist/features.cjs +34 -41
  44. package/dist/features.js +2 -2
  45. package/dist/federation/federate.cjs +37 -44
  46. package/dist/federation/federate.js +2 -2
  47. package/dist/generate.cjs +37 -42
  48. package/dist/generate.js +4 -4
  49. package/dist/resolve-catalog-dependencies.cjs +34 -41
  50. package/dist/resolve-catalog-dependencies.js +2 -2
  51. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  52. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  53. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  54. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  55. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  56. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  57. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  58. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  59. package/dist/runtime-dependencies/manifest.json +13 -1
  60. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  61. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  62. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  63. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  64. package/dist/runtime-dependencies/yjs.mjs +3 -0
  65. package/dist/utils/child-process-signals.cjs +53 -0
  66. package/dist/utils/child-process-signals.d.cts +19 -0
  67. package/dist/utils/child-process-signals.d.ts +19 -0
  68. package/dist/utils/child-process-signals.js +6 -0
  69. package/dist/utils/cli-logger.cjs +1 -1
  70. package/dist/utils/cli-logger.js +2 -2
  71. package/dist/utils/license-status.cjs +78 -45
  72. package/dist/utils/license-status.d.cts +15 -5
  73. package/dist/utils/license-status.d.ts +15 -5
  74. package/dist/utils/license-status.js +1 -1
  75. package/eventcatalog/astro.config.mjs +23 -1
  76. package/eventcatalog/integrations/config-loader.mjs +56 -58
  77. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  78. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  79. package/eventcatalog/integrations/storage.mjs +20 -0
  80. package/eventcatalog/integrations/studio-server.mjs +32 -0
  81. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  82. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  83. package/eventcatalog/src/components/Header.astro +3 -3
  84. package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
  85. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
  86. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  87. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  88. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  89. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  90. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  91. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  92. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  93. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  94. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  95. package/eventcatalog/src/components/Seo.astro +5 -1
  96. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
  97. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
  98. package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
  99. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
  100. package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
  101. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
  102. package/eventcatalog/src/components/Settings/Row.tsx +7 -4
  103. package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
  104. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  105. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  106. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  107. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  108. package/eventcatalog/src/components/TrialBadge.astro +20 -9
  109. package/eventcatalog/src/content.config.ts +19 -2
  110. package/eventcatalog/src/features/auth/error.astro +4 -2
  111. package/eventcatalog/src/features/auth/login.astro +2 -1
  112. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  113. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  114. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  115. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  116. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  117. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  118. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  119. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  120. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  121. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +65 -65
  122. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  123. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  124. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  125. package/eventcatalog/src/features/server/start.ts +25 -0
  126. package/eventcatalog/src/features/storage/database.ts +88 -0
  127. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  128. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  129. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  130. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  131. package/eventcatalog/src/features/storage/sql.ts +28 -0
  132. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  133. package/eventcatalog/src/features/studio/agents.ts +28 -0
  134. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  135. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  136. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  137. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  138. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  139. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  140. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  141. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  142. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  143. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  144. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  145. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  146. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  147. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  148. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +206 -0
  149. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  150. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  151. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  152. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  153. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  154. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  155. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  156. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  157. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  158. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  159. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  160. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  161. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  162. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  163. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  164. package/eventcatalog/src/features/studio/identity.ts +59 -0
  165. package/eventcatalog/src/features/studio/limits.ts +2 -0
  166. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  167. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  168. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  169. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  170. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  171. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  172. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  173. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  174. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  175. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  176. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  177. package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
  178. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -5
  179. package/eventcatalog/src/pages/_index.astro +5 -342
  180. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
  181. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
  182. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
  183. package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
  184. package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
  185. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
  186. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
  187. package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
  188. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
  189. package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
  190. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
  191. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
  192. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
  193. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
  194. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
  195. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
  196. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
  197. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
  198. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
  199. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
  200. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
  203. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
  204. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
  205. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
  206. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
  207. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
  208. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
  209. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
  210. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
  211. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
  212. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
  213. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
  214. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
  215. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
  216. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
  217. package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
  218. package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
  219. package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
  220. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
  221. package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
  222. package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
  223. package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
  224. package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
  225. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
  226. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
  227. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
  228. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
  229. package/eventcatalog/src/pages/index.astro +4 -2
  230. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
  231. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +1 -0
  232. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
  233. package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
  234. package/eventcatalog/src/pages/settings/license.astro +9 -1
  235. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
  236. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
  237. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
  238. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
  239. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
  240. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
  241. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
  242. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
  243. package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
  244. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
  245. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
  246. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
  247. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
  248. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
  249. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
  250. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
  251. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
  252. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
  253. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
  254. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
  255. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  256. package/eventcatalog/src/utils/collections/util.ts +8 -4
  257. package/eventcatalog/src/utils/feature.ts +8 -0
  258. package/eventcatalog/src/utils/git.ts +22 -5
  259. package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
  260. package/eventcatalog/src/utils/license.ts +1 -1
  261. package/eventcatalog/src/utils/link-validation.ts +25 -19
  262. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  263. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  264. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  265. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
  266. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  267. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  268. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  269. package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
  270. package/eventcatalog/src/utils/page-title.ts +15 -0
  271. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  272. package/eventcatalog/src/utils/trial.ts +14 -1
  273. package/package.json +19 -13
  274. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  275. package/eventcatalog/public/studio-bg.png +0 -0
  276. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  277. package/eventcatalog/src/pages/studio.astro +0 -316
@@ -0,0 +1,293 @@
1
+ import { Fragment, memo, useEffect, useLayoutEffect, useRef, useSyncExternalStore, type MutableRefObject } from 'react';
2
+ import { useInternalNode, useStore, useStoreApi, type XYPosition } from '@xyflow/react';
3
+ import { Eye, EyeOff } from 'lucide-react';
4
+ import { CanvasPortal, sizeOf, COUNTER_SCALE, useCounterScale, Picture, AgentMark } from '@eventcatalog/visualiser/studio';
5
+ import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
6
+ import { useFollowCamera } from '../hooks/use-follow-camera';
7
+
8
+ /**
9
+ * Other people on the canvas: their pointers, selections and the connections they're dragging. Drawn in the
10
+ * canvas's own coordinates (React Flow's viewport portal), so panning and zooming move them without
11
+ * re-rendering, and counter-scaled (COUNTER_SCALE, set outside React) so they stay the same size on screen. Each
12
+ * person's pointer and marks follow them on their own, so one pointer moving doesn't re-render anyone else's.
13
+ */
14
+ export const RemotePresence = memo(function RemotePresence({
15
+ store,
16
+ clientId,
17
+ }: {
18
+ store: PresenceStore | null;
19
+ clientId: number | null;
20
+ }) {
21
+ const people = usePeople(store);
22
+ const layer = useRef<HTMLDivElement>(null);
23
+ useCounterScale(layer);
24
+ const others = store ? people.filter((peer) => peer.clientId !== clientId) : [];
25
+
26
+ return (
27
+ <CanvasPortal>
28
+ {/* Above nodes (selected ones are raised to 1000) */}
29
+ <div ref={layer} className="pointer-events-none absolute left-0 top-0" style={{ zIndex: 2000 }}>
30
+ {others.map((peer) => (
31
+ <Fragment key={peer.clientId}>
32
+ <PeerMarks store={store!} clientId={peer.clientId} />
33
+ {peer.pointer && <PeerPointer store={store!} peer={peer} />}
34
+ </Fragment>
35
+ ))}
36
+ </div>
37
+ </CanvasPortal>
38
+ );
39
+ });
40
+
41
+ const findPeer = (store: PresenceStore, clientId: number) => store.getPeers().find((peer) => peer.clientId === clientId);
42
+
43
+ /** What a peer's marks show: re-rendered only when it changes */
44
+ const marksKey = (peer: Peer | undefined) =>
45
+ peer
46
+ ? [
47
+ peer.selection.join(','),
48
+ peer.connecting && peer.pointer ? [peer.connecting.x, peer.connecting.y, peer.pointer.x, peer.pointer.y].join(',') : '',
49
+ ].join('|')
50
+ : '';
51
+
52
+ /** Someone's selection, and the connection they're dragging (a dashed line from where it starts to their pointer) */
53
+ const PeerMarks = memo(function PeerMarks({ store, clientId }: { store: PresenceStore; clientId: number }) {
54
+ useSyncExternalStore(store.subscribe, () => marksKey(findPeer(store, clientId)));
55
+ const peer = findPeer(store, clientId);
56
+ if (!peer) return null;
57
+ return (
58
+ <>
59
+ {peer.selection.map((id) => (
60
+ <SelectionOutline key={id} nodeId={id} name={peer.name} color={peer.color} />
61
+ ))}
62
+ {peer.pointer && peer.connecting && <ConnectingLine from={peer.connecting} to={peer.pointer} color={peer.color} />}
63
+ </>
64
+ );
65
+ });
66
+
67
+ /** A connection someone's dragging: only shown while they drag one, so it can follow the zoom itself */
68
+ function ConnectingLine({ from, to, color }: { from: XYPosition; to: XYPosition; color: string }) {
69
+ const scale = 1 / useStore((state) => state.transform[2]);
70
+ return (
71
+ <svg className="absolute left-0 top-0 overflow-visible" width={1} height={1}>
72
+ <line
73
+ x1={from.x}
74
+ y1={from.y}
75
+ x2={to.x}
76
+ y2={to.y}
77
+ stroke={color}
78
+ strokeWidth={2 * scale}
79
+ strokeDasharray={`${6 * scale} ${4 * scale}`}
80
+ />
81
+ <circle cx={from.x} cy={from.y} r={4 * scale} fill={color} />
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ /** Longest a pointer takes to glide to where it was last seen (updates come about every 33 ms) */
87
+ const MAX_GLIDE_MS = 120;
88
+
89
+ /**
90
+ * Someone's pointer, gliding from where it was to each new position over the time since the last one (so a
91
+ * steady stream of updates looks like steady movement). Moved with a transform outside React: no re-renders.
92
+ */
93
+ const PeerPointer = memo(function PeerPointer({ store, peer }: { store: PresenceStore; peer: Peer }) {
94
+ const ref = useRef<HTMLDivElement>(null);
95
+ const shown = useRef<XYPosition | null>(null);
96
+
97
+ const draw = () => {
98
+ if (ref.current && shown.current)
99
+ ref.current.style.transform = `translate(${shown.current.x}px, ${shown.current.y}px) scale(var(${COUNTER_SCALE}, 1))`;
100
+ };
101
+
102
+ useLayoutEffect(() => {
103
+ let target: XYPosition | null = null;
104
+ let from: XYPosition = { x: 0, y: 0 };
105
+ let start = 0;
106
+ let duration = 0;
107
+ let lastSeen = 0;
108
+ let frame = 0;
109
+
110
+ const step = (now: number) => {
111
+ const t = Math.min(1, (now - start) / duration);
112
+ if (target) shown.current = { x: from.x + (target.x - from.x) * t, y: from.y + (target.y - from.y) * t };
113
+ draw();
114
+ frame = t < 1 ? requestAnimationFrame(step) : 0;
115
+ };
116
+
117
+ const follow = () => {
118
+ const pointer = findPeer(store, peer.clientId)?.pointer;
119
+ if (!pointer || (target && pointer.x === target.x && pointer.y === target.y)) return;
120
+ const now = performance.now();
121
+ target = pointer;
122
+ if (!shown.current) {
123
+ shown.current = pointer;
124
+ lastSeen = now;
125
+ return draw();
126
+ }
127
+ from = shown.current;
128
+ start = now;
129
+ duration = Math.min(MAX_GLIDE_MS, Math.max(16, now - lastSeen));
130
+ lastSeen = now;
131
+ if (!frame) frame = requestAnimationFrame(step);
132
+ };
133
+
134
+ follow();
135
+ const unsubscribe = store.subscribe(follow);
136
+ return () => {
137
+ unsubscribe();
138
+ cancelAnimationFrame(frame);
139
+ };
140
+ }, [store, peer.clientId]);
141
+
142
+ return (
143
+ <div ref={ref} className="absolute left-0 top-0 will-change-transform" style={{ transformOrigin: '0 0' }}>
144
+ {/* The pointer's outline is a light border on any background */}
145
+ <svg width="18" height="18" viewBox="0 0 24 24" fill={peer.color} stroke="white" strokeWidth="1.5">
146
+ <path d="M4 2l16 9-7 2-3 7z" />
147
+ </svg>
148
+ <span
149
+ className="absolute left-4 top-4 flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium text-white"
150
+ style={{ backgroundColor: peer.color }}
151
+ >
152
+ {peer.agent && <AgentMark name={peer.name} size={12} />}
153
+ <Picture src={peer.picture} className="-ml-0.5 h-4 w-4 shrink-0" />
154
+ {peer.name}
155
+ {peer.activity && <span className="font-normal opacity-90">· {peer.activity}</span>}
156
+ </span>
157
+ </div>
158
+ );
159
+ });
160
+
161
+ // Screen pixels, in the canvas's coordinates (counter-scaled by the layer's COUNTER_SCALE)
162
+ const onScreen = (px: number) => `calc(${px}px * var(${COUNTER_SCALE}, 1))`;
163
+
164
+ /** Someone's selection around a node: follows that node only, so other nodes moving don't re-render it */
165
+ const SelectionOutline = memo(function SelectionOutline({
166
+ nodeId,
167
+ name,
168
+ color,
169
+ }: {
170
+ nodeId: string;
171
+ name: string;
172
+ color: string;
173
+ }) {
174
+ const node = useInternalNode(nodeId);
175
+ if (!node) return null;
176
+ const { width, height } = sizeOf(node);
177
+ const { x, y } = node.internals.positionAbsolute;
178
+ return (
179
+ <div
180
+ className="absolute"
181
+ style={{
182
+ left: `calc(${x}px - ${onScreen(4)})`,
183
+ top: `calc(${y}px - ${onScreen(4)})`,
184
+ width: `calc(${width}px + ${onScreen(8)})`,
185
+ height: `calc(${height}px + ${onScreen(8)})`,
186
+ border: `${onScreen(2)} solid ${color}`,
187
+ borderRadius: onScreen(8),
188
+ }}
189
+ >
190
+ <span
191
+ className="absolute right-0 top-0 whitespace-nowrap rounded px-1.5 text-[10px] leading-4 text-white"
192
+ style={{
193
+ backgroundColor: color,
194
+ transform: `translateY(-100%) scale(var(${COUNTER_SCALE}, 1))`,
195
+ transformOrigin: 'bottom right',
196
+ }}
197
+ >
198
+ {name}
199
+ </span>
200
+ </div>
201
+ );
202
+ });
203
+
204
+ /**
205
+ * Shares what we're looking at, so others can jump to it: the canvas position in the middle of the screen,
206
+ * and the zoom. Follows React Flow's store (people's moves and code's, e.g. fit view) without rendering.
207
+ */
208
+ export const ViewportSharer = memo(function ViewportSharer({
209
+ onChange,
210
+ }: {
211
+ onChange: (viewport: { x: number; y: number; zoom: number }) => void;
212
+ }) {
213
+ const store = useStoreApi();
214
+ useEffect(() => {
215
+ let timer: ReturnType<typeof setTimeout> | undefined;
216
+ const share = () => {
217
+ const {
218
+ transform: [x, y, zoom],
219
+ width,
220
+ height,
221
+ } = store.getState();
222
+ onChange({ x: (width / 2 - x) / zoom, y: (height / 2 - y) / zoom, zoom });
223
+ };
224
+ const unsubscribe = store.subscribe((state, previous) => {
225
+ if (state.transform === previous.transform && state.width === previous.width && state.height === previous.height) return;
226
+ clearTimeout(timer);
227
+ timer = setTimeout(share, 150);
228
+ });
229
+ share();
230
+ return () => {
231
+ unsubscribe();
232
+ clearTimeout(timer);
233
+ };
234
+ }, [store, onChange]);
235
+ return null;
236
+ });
237
+
238
+ /**
239
+ * Agents at work: who's working (with a button to follow them or stop), and the camera following them while
240
+ * they work (on by default inside a chat). `pause` is set for whoever moves the camera themselves.
241
+ */
242
+ export const AgentActivity = memo(function AgentActivity({
243
+ store,
244
+ clientId,
245
+ enabled,
246
+ following,
247
+ onToggleFollowing,
248
+ pauseRef,
249
+ }: {
250
+ store: PresenceStore | null;
251
+ clientId: number | null;
252
+ /** Off in the read only levels */
253
+ enabled: boolean;
254
+ following: boolean;
255
+ onToggleFollowing: () => void;
256
+ pauseRef: MutableRefObject<() => void>;
257
+ }) {
258
+ const people = usePeople(store);
259
+ const working = people.filter((peer) => peer.clientId !== clientId && peer.agent && peer.pointer);
260
+ const camera = useFollowCamera({
261
+ enabled: following && enabled,
262
+ fitsContent: enabled,
263
+ agentsWorking: working.length > 0,
264
+ getAgents: () => (store?.getPeers() ?? []).filter((peer: Peer) => peer.clientId !== clientId && peer.agent && peer.pointer),
265
+ });
266
+ pauseRef.current = camera.pause;
267
+
268
+ if (!enabled || working.length === 0) return null;
269
+ return (
270
+ <div className="absolute left-1/2 top-16 z-10 flex -translate-x-1/2 items-center gap-2 rounded-full border px-3 py-1 text-xs shadow-sm bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
271
+ <span className="flex -space-x-1">
272
+ {working.map((agent) => (
273
+ <span
274
+ key={agent.clientId}
275
+ className="h-2.5 w-2.5 rounded-full ring-2 ring-[rgb(var(--ec-card-bg))]"
276
+ style={{ backgroundColor: agent.color }}
277
+ />
278
+ ))}
279
+ </span>
280
+ <span>
281
+ {following ? 'Following' : ''} <span className="font-semibold">{working.map((agent) => agent.name).join(', ')}</span>
282
+ {following ? '' : ` ${working.length === 1 ? 'is' : 'are'} working`}
283
+ </span>
284
+ <button
285
+ onClick={onToggleFollowing}
286
+ className="flex items-center gap-1 rounded-full px-2 py-0.5 font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
287
+ >
288
+ {following ? <EyeOff size={12} /> : <Eye size={12} />}
289
+ {following ? 'Stop' : 'Follow'}
290
+ </button>
291
+ </div>
292
+ );
293
+ });
@@ -0,0 +1,396 @@
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import {
3
+ ArrowLeft,
4
+ Bot,
5
+ Check,
6
+ Copy,
7
+ Globe,
8
+ Link2,
9
+ Mail,
10
+ MessageSquareText,
11
+ Pencil,
12
+ Plug,
13
+ UserPlus,
14
+ Users,
15
+ X,
16
+ } from 'lucide-react';
17
+ import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
18
+ import { Picture, AgentMark } from '@eventcatalog/visualiser/studio';
19
+
20
+ /**
21
+ * Sharing a canvas, as Studio on the web does: first the ways to share, side by side (a link to it, working on it
22
+ * together live, pairing with an AI agent), then, for live collaboration, who's on it now (click someone to go to
23
+ * what they're looking at) and the link to invite people with (copied, emailed or as a ready-made message). A canvas
24
+ * here is always live and kept in the catalog: its link is the invite, so there's no session to start.
25
+ */
26
+
27
+ const button =
28
+ 'inline-flex h-9 flex-none items-center gap-2 rounded-lg px-4 text-sm font-medium transition-colors [&_svg]:h-4 [&_svg]:w-4';
29
+ const primary = `${button} bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-page-text)/0.85)]`;
30
+ const secondary = `${button} border border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))]`;
31
+ const sectionLabel = 'text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]';
32
+
33
+ /** e.g. catalog.example.com/studio/9b1d… */
34
+ function shortened(url: string) {
35
+ try {
36
+ const { host, pathname } = new URL(url);
37
+ return `${host}${pathname}`;
38
+ } catch {
39
+ return url;
40
+ }
41
+ }
42
+
43
+ const copyText = async (text: string) => {
44
+ try {
45
+ await navigator.clipboard.writeText(text);
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ };
51
+
52
+ /** One of the ways to share, as a card: what it is, what it gives them, and what to do */
53
+ function Choice({
54
+ icon,
55
+ title,
56
+ description,
57
+ points,
58
+ children,
59
+ }: {
60
+ icon: ReactNode;
61
+ title: string;
62
+ description: string;
63
+ points: string[];
64
+ children: ReactNode;
65
+ }) {
66
+ return (
67
+ <section className="flex flex-col rounded-xl border border-[rgb(var(--ec-page-border))] p-5 text-left transition-colors hover:border-[color-mix(in_srgb,rgb(var(--ec-accent))_45%,rgb(var(--ec-page-border)))]">
68
+ <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-[color-mix(in_srgb,rgb(var(--ec-accent))_12%,transparent)] text-[rgb(var(--ec-accent))] [&_svg]:h-[18px] [&_svg]:w-[18px]">
69
+ {icon}
70
+ </span>
71
+ <h3 className="mt-4 text-sm font-semibold text-[rgb(var(--ec-page-text))]">{title}</h3>
72
+ <p className="mt-1 text-[13px] leading-relaxed text-[rgb(var(--ec-page-text-muted))]">{description}</p>
73
+ <ul className="mt-4 space-y-2">
74
+ {points.map((point) => (
75
+ <li key={point} className="flex items-start gap-2 text-xs leading-snug text-[rgb(var(--ec-page-text-muted))]">
76
+ <Check aria-hidden className="mt-px h-3.5 w-3.5 flex-none text-emerald-500" />
77
+ {point}
78
+ </li>
79
+ ))}
80
+ </ul>
81
+ <div className="mt-auto pt-6">{children}</div>
82
+ </section>
83
+ );
84
+ }
85
+
86
+ /** Copies some text, saying it did (or that it couldn't) */
87
+ function CopyButton({ text, label, icon, className }: { text: string; label: string; icon: ReactNode; className: string }) {
88
+ const [copied, setCopied] = useState<boolean | null>(null);
89
+ useEffect(() => {
90
+ if (copied === null) return;
91
+ const timer = setTimeout(() => setCopied(null), 1500);
92
+ return () => clearTimeout(timer);
93
+ }, [copied]);
94
+ return (
95
+ <button type="button" onClick={async () => setCopied(await copyText(text))} className={className}>
96
+ {copied ? <Check aria-hidden /> : icon}
97
+ {copied ? 'Copied' : copied === false ? "Couldn't copy" : label}
98
+ </button>
99
+ );
100
+ }
101
+
102
+ export default function ShareDialog({
103
+ url,
104
+ title,
105
+ keptInMemory = false,
106
+ presence,
107
+ clientId,
108
+ onRename,
109
+ onJumpTo,
110
+ onConnectAgent,
111
+ onClose,
112
+ initialView = 'choose',
113
+ }: {
114
+ /** Straight to live collaboration (who's here, and the invite), e.g. from the menu */
115
+ initialView?: 'choose' | 'invite';
116
+ url: string;
117
+ title?: string;
118
+ /** Canvases are lost when the server restarts */
119
+ keptInMemory?: boolean;
120
+ presence: PresenceStore | null;
121
+ clientId: number | null;
122
+ /** Not given when you're signed in */
123
+ onRename?: (name: string) => void;
124
+ onJumpTo: (peer: Peer) => void;
125
+ onConnectAgent: () => void;
126
+ onClose: () => void;
127
+ }) {
128
+ const people = usePeople(presence);
129
+ const you = people.find((peer) => peer.clientId === clientId);
130
+ const others = people.filter((peer) => peer.clientId !== clientId);
131
+ const name = title || 'this canvas';
132
+ const invite = `I'm designing ${title ? `"${title}"` : 'an architecture'} in EventCatalog Studio. Join me, we can work on it together live: ${url}`;
133
+ const email = `mailto:?subject=${encodeURIComponent(`Join me on ${name} in EventCatalog Studio`)}&body=${encodeURIComponent(invite)}`;
134
+ // The ways to share first; live collaboration opens who's here and the invite
135
+ const [view, setView] = useState<'choose' | 'invite'>(initialView);
136
+ const choosing = view === 'choose';
137
+ const wide = 'w-full justify-center';
138
+
139
+ useEffect(() => {
140
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onClose();
141
+ window.addEventListener('keydown', onKeyDown);
142
+ return () => window.removeEventListener('keydown', onKeyDown);
143
+ }, [onClose]);
144
+
145
+ return (
146
+ <div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
147
+ <div
148
+ role="dialog"
149
+ aria-modal="true"
150
+ aria-labelledby="share-canvas-title"
151
+ onClick={(event) => event.stopPropagation()}
152
+ className={`w-full ${choosing ? 'max-w-4xl' : 'max-w-lg'} rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] text-[rgb(var(--ec-page-text))] shadow-2xl`}
153
+ >
154
+ <div className="flex items-start justify-between gap-3 px-6 pt-5">
155
+ <div className="flex min-w-0 items-start gap-2">
156
+ {!choosing && (
157
+ <button
158
+ type="button"
159
+ aria-label="Back to the ways to share"
160
+ onClick={() => setView('choose')}
161
+ className="-ml-1.5 mt-0.5 rounded-md p-1 text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))]"
162
+ >
163
+ <ArrowLeft aria-hidden className="h-4 w-4" />
164
+ </button>
165
+ )}
166
+ <div className="min-w-0">
167
+ <h2 id="share-canvas-title" className="truncate text-base font-semibold">
168
+ {choosing ? 'Share' : 'Live collaboration'}
169
+ </h2>
170
+ <p className="mt-1 text-sm text-[rgb(var(--ec-page-text-muted))]">
171
+ {choosing
172
+ ? `Choose how to work on ${title ? `"${title}"` : 'this canvas'} with others.`
173
+ : "Everyone with the link joins live: you see each other's pointers and changes as they happen."}
174
+ </p>
175
+ </div>
176
+ </div>
177
+ <button
178
+ type="button"
179
+ aria-label="Close"
180
+ onClick={onClose}
181
+ className="-mr-1 rounded-md p-1.5 text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))]"
182
+ >
183
+ <X aria-hidden className="h-4 w-4" />
184
+ </button>
185
+ </div>
186
+
187
+ {choosing ? (
188
+ <div className="grid gap-4 px-6 pb-6 pt-5 md:grid-cols-3">
189
+ <Choice
190
+ icon={<Link2 aria-hidden />}
191
+ title="Share a link"
192
+ description="A link to this canvas, in your catalog."
193
+ points={[
194
+ 'Teammates open it in EventCatalog',
195
+ 'Paste it to your AI agent to build on',
196
+ keptInMemory ? "Kept in this EventCatalog's memory, until it restarts" : 'Kept in your catalog',
197
+ ]}
198
+ >
199
+ <CopyButton text={url} label="Copy link" icon={<Link2 aria-hidden />} className={`${secondary} ${wide}`} />
200
+ </Choice>
201
+
202
+ <Choice
203
+ icon={<Users aria-hidden />}
204
+ title="Live collaboration"
205
+ description="Work on this canvas together, in real time."
206
+ points={[
207
+ "See each other's pointers and edits",
208
+ 'Comment and talk it through',
209
+ `${people.length} ${people.length === 1 ? 'person' : 'people'} here now`,
210
+ ]}
211
+ >
212
+ <button type="button" onClick={() => setView('invite')} className={`${primary} ${wide}`}>
213
+ <UserPlus aria-hidden /> Invite people
214
+ </button>
215
+ </Choice>
216
+
217
+ <Choice
218
+ icon={<Bot aria-hidden />}
219
+ title="Pair with your AI agent"
220
+ description="Sketch ideas with your agent, or have it explain how something works."
221
+ points={['Draws and edits the canvas with you', 'Claude, ChatGPT, Cursor and more']}
222
+ >
223
+ <button
224
+ type="button"
225
+ onClick={() => {
226
+ onClose();
227
+ onConnectAgent();
228
+ }}
229
+ className={`${secondary} ${wide}`}
230
+ >
231
+ <Plug aria-hidden /> Connect agent
232
+ </button>
233
+ </Choice>
234
+ </div>
235
+ ) : (
236
+ <>
237
+ <div className="space-y-5 px-6 pb-5 pt-4">
238
+ {/* Who's here */}
239
+ <section>
240
+ <div className="flex items-center justify-between">
241
+ <h3 className={sectionLabel}>On this canvas</h3>
242
+ <span className="inline-flex items-center gap-1.5 text-xs text-[rgb(var(--ec-page-text-muted))]">
243
+ <span className="h-2 w-2 rounded-full bg-emerald-500" /> {people.length}{' '}
244
+ {people.length === 1 ? 'person' : 'people'} here
245
+ </span>
246
+ </div>
247
+ <ul className="mt-2 flex max-h-40 flex-wrap gap-2 overflow-y-auto">
248
+ {you && <YouChip peer={you} onRename={onRename} />}
249
+ {others.map((peer) => (
250
+ <li key={peer.clientId}>
251
+ <button
252
+ type="button"
253
+ title={`Go to what ${peer.name} is looking at${peer.activity ? ` (${peer.activity})` : ''}`}
254
+ onClick={() => {
255
+ onJumpTo(peer);
256
+ onClose();
257
+ }}
258
+ className="inline-flex items-center gap-2 rounded-full border border-[rgb(var(--ec-page-border))] py-1 pl-1 pr-3 text-sm hover:border-[rgb(var(--ec-accent))]"
259
+ >
260
+ <Avatar peer={peer} />
261
+ {peer.name}
262
+ {peer.agent && (
263
+ <span className="rounded px-1 py-px text-[10px] font-medium bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]">
264
+ AI agent
265
+ </span>
266
+ )}
267
+ </button>
268
+ </li>
269
+ ))}
270
+ </ul>
271
+ {others.length === 0 && (
272
+ <p className="mt-2 text-xs text-[rgb(var(--ec-page-text-muted))]">
273
+ Just you so far. Send the link to bring people in.
274
+ </p>
275
+ )}
276
+ </section>
277
+
278
+ {/* Inviting people */}
279
+ <section>
280
+ <h3 className={sectionLabel}>Invite people</h3>
281
+ <p className="mt-1 text-sm text-[rgb(var(--ec-page-text-muted))]">
282
+ Anyone with this link joins and edits this canvas with you.
283
+ </p>
284
+ <div className="mt-2 flex w-full items-center gap-2">
285
+ <div
286
+ title={url}
287
+ className="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover))] px-3 text-sm text-[rgb(var(--ec-page-text-muted))]"
288
+ >
289
+ <Link2 aria-hidden className="h-4 w-4 flex-none" />
290
+ <span className="truncate font-mono text-xs">{shortened(url)}</span>
291
+ </div>
292
+ <CopyButton text={url} label="Copy invite" icon={<Copy aria-hidden />} className={primary} />
293
+ </div>
294
+ <div className="mt-2 flex flex-wrap gap-2">
295
+ <a href={email} className={`${secondary} h-8 px-3 text-xs`}>
296
+ <Mail aria-hidden /> Email an invite
297
+ </a>
298
+ <CopyButton
299
+ text={invite}
300
+ label="Copy invite message"
301
+ icon={<MessageSquareText aria-hidden />}
302
+ className={`${secondary} h-8 px-3 text-xs`}
303
+ />
304
+ </div>
305
+ </section>
306
+ </div>
307
+
308
+ <div className="flex items-center justify-between gap-3 border-t border-[rgb(var(--ec-page-border))] px-6 py-3">
309
+ <p className="flex items-start gap-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
310
+ <Globe aria-hidden className="mt-px h-3 w-3 flex-none" />
311
+ <span>
312
+ Anyone with the link can view and edit.
313
+ {keptInMemory && " Canvases are kept in this EventCatalog's memory, so they're lost when it restarts."}
314
+ </span>
315
+ </p>
316
+ <button type="button" onClick={onClose} className={secondary}>
317
+ Done
318
+ </button>
319
+ </div>
320
+ </>
321
+ )}
322
+ </div>
323
+ </div>
324
+ );
325
+ }
326
+
327
+ const initials = (name: string) =>
328
+ name
329
+ .split(/\s+/)
330
+ .filter(Boolean)
331
+ .slice(0, 2)
332
+ .map((part) => part[0]!.toUpperCase())
333
+ .join('');
334
+
335
+ function Avatar({ peer }: { peer: Peer }) {
336
+ return (
337
+ <span
338
+ className="relative flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
339
+ style={{ backgroundColor: peer.color }}
340
+ >
341
+ {peer.agent ? <AgentMark name={peer.name} size={12} /> : initials(peer.name)}
342
+ <Picture src={peer.picture} />
343
+ </span>
344
+ );
345
+ }
346
+
347
+ /** You, with your name (the name others see you by, which you can change unless you're signed in) */
348
+ function YouChip({ peer, onRename }: { peer: Peer; onRename?: (name: string) => void }) {
349
+ const [editing, setEditing] = useState(false);
350
+ const [draft, setDraft] = useState(peer.name);
351
+ const save = () => {
352
+ const trimmed = draft.trim();
353
+ if (trimmed && trimmed !== peer.name) onRename?.(trimmed);
354
+ setEditing(false);
355
+ };
356
+ return (
357
+ <li className="inline-flex items-center gap-2 rounded-full border border-[rgb(var(--ec-page-border))] py-1 pl-1 pr-2 text-sm">
358
+ <Avatar peer={peer} />
359
+ {editing ? (
360
+ <input
361
+ autoFocus
362
+ value={draft}
363
+ onChange={(event) => setDraft(event.target.value)}
364
+ onBlur={save}
365
+ onKeyDown={(event) => {
366
+ if (event.key === 'Enter') save();
367
+ if (event.key === 'Escape') {
368
+ event.stopPropagation();
369
+ setEditing(false);
370
+ }
371
+ }}
372
+ aria-label="Your name"
373
+ className="w-36 rounded-md border px-2 py-0.5 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
374
+ />
375
+ ) : (
376
+ <>
377
+ {peer.name} <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">(you)</span>
378
+ </>
379
+ )}
380
+ {!editing && onRename && (
381
+ <button
382
+ type="button"
383
+ aria-label="Change your name"
384
+ title="Change the name others see you by"
385
+ onClick={() => {
386
+ setDraft(peer.name);
387
+ setEditing(true);
388
+ }}
389
+ className="rounded-full p-1 text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))]"
390
+ >
391
+ <Pencil aria-hidden className="h-3 w-3" />
392
+ </button>
393
+ )}
394
+ </li>
395
+ );
396
+ }