@eventcatalog/core 5.0.0-beta.9 → 5.0.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.
Files changed (460) hide show
  1. package/LICENSE +13 -12
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +1 -1
  4. package/dist/analytics/analytics.js +2 -2
  5. package/dist/analytics/log-build.cjs +91 -44
  6. package/dist/analytics/log-build.d.cts +2 -2
  7. package/dist/analytics/log-build.d.ts +2 -2
  8. package/dist/analytics/log-build.js +6 -5
  9. package/dist/analytics/studio-usage.cjs +90 -0
  10. package/dist/analytics/studio-usage.d.cts +5 -0
  11. package/dist/analytics/studio-usage.d.ts +5 -0
  12. package/dist/analytics/studio-usage.js +10 -0
  13. package/dist/catalog-runtime.cjs +1 -1
  14. package/dist/{chunk-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
  15. package/dist/{chunk-AWOO63ME.js → chunk-AD7CL772.js} +11 -4
  16. package/dist/chunk-CA6WCQ2T.js +54 -0
  17. package/dist/{chunk-GAHKCOQM.js → chunk-JZN4RSMF.js} +1 -1
  18. package/dist/{chunk-JQMYHZWI.js → chunk-NEAVAFSJ.js} +16 -8
  19. package/dist/{chunk-4CFZUHGG.js → chunk-NIVST4WY.js} +1 -1
  20. package/dist/{chunk-ENAG6UH7.js → chunk-NKNOXQ62.js} +1 -6
  21. package/dist/{chunk-ZEY2XUJN.js → chunk-OX6TSVHY.js} +6 -6
  22. package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
  23. package/dist/{chunk-EUNQJO3J.js → chunk-RE7UTPWH.js} +1 -1
  24. package/dist/chunk-S5GWAUG5.js +29 -0
  25. package/dist/{chunk-TZVDMWQX.js → chunk-YQGRMDRB.js} +4 -2
  26. package/dist/constants.cjs +1 -1
  27. package/dist/constants.js +1 -1
  28. package/dist/docs/api/02-config.md +57 -7
  29. package/dist/docs/development/00-why-eventcatalog.md +11 -11
  30. package/dist/docs/development/01-fundamentals.md +20 -13
  31. package/dist/docs/development/01-getting-started/configuration-overview.md +2 -2
  32. package/dist/docs/development/01-getting-started/installation.md +9 -1
  33. package/dist/docs/development/_getting-started.mdx +1 -1
  34. package/dist/docs/development/ask-your-architecture/01-intro.md +5 -5
  35. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md +0 -4
  36. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +15 -3
  37. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/03-bring-your-own-tools.md +0 -3
  38. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json +1 -1
  39. package/dist/docs/development/ask-your-architecture/02-llms.txt.md +1 -1
  40. package/dist/docs/development/ask-your-architecture/{05-skills/01-introduction.md → 03-install-skills.md} +57 -7
  41. package/dist/docs/development/ask-your-architecture/03-mcp-server/_category_.json +1 -1
  42. package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +86 -33
  43. package/dist/docs/development/ask-your-architecture/03-mcp-server/introduction.md +14 -4
  44. package/dist/docs/development/ask-your-architecture/03-schemas.txt.md +1 -1
  45. package/dist/docs/development/ask-your-architecture/04-agents/_category_.json +1 -1
  46. package/dist/docs/development/authentication/01-introduction.md +2 -23
  47. package/dist/docs/development/authentication/02-enabling-authentication.md +0 -20
  48. package/dist/docs/development/authentication/07-rbac-middleware.md +0 -2
  49. package/dist/docs/development/authentication/providers/03-setting-up-github.md +0 -2
  50. package/dist/docs/development/authentication/providers/03a-setting-up-google.md +0 -2
  51. package/dist/docs/development/authentication/providers/04-setting-up-azure-ad.md +0 -2
  52. package/dist/docs/development/authentication/providers/05-setting-up-okta.md +0 -3
  53. package/dist/docs/development/authentication/providers/06-setting-up-auth0.md +0 -2
  54. package/dist/docs/development/bring-your-own-documentation/01-introduction.md +0 -4
  55. package/dist/docs/development/bring-your-own-documentation/custom-pages/01-introduction.md +0 -3
  56. package/dist/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md +0 -17
  57. package/dist/docs/development/bring-your-own-documentation/custom-pages/03-components.md +0 -4
  58. package/dist/docs/development/bring-your-own-documentation/custom-pages/04-owners.md +0 -2
  59. package/dist/docs/development/bring-your-own-documentation/resource-docs/01-introduction.md +1 -3
  60. package/dist/docs/development/bring-your-own-documentation/resource-docs/02-adding-resource-docs.md +1 -4
  61. package/dist/docs/development/bring-your-own-documentation/resource-docs/03-categories.md +0 -2
  62. package/dist/docs/development/bring-your-own-documentation/resource-docs/04-versioning.md +1 -3
  63. package/dist/docs/development/components/components/11-flow.md +17 -0
  64. package/dist/docs/development/components/components/15-nodegraph.md +43 -10
  65. package/dist/docs/development/components/components/18-remote-schema.md +1 -4
  66. package/dist/docs/development/components/custom-components/07-reference.md +14 -0
  67. package/dist/docs/development/customization/02-themes.md +0 -5
  68. package/dist/docs/development/customization/03-search.md +1 -1
  69. package/dist/docs/development/customization/05-custom-pages-and-api-routes/01-introduction.md +0 -2
  70. package/dist/docs/development/customization/05-custom-pages-and-api-routes/04-reference.md +7 -1
  71. package/dist/docs/development/customization/05-resource-sidebar.md +1 -1
  72. package/dist/docs/development/customization/09-embed-eventcatalog.md +20 -25
  73. package/dist/docs/development/deployment/hosting-options.md +7 -2
  74. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/05-fix-common-problems.md +16 -0
  75. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/03-rules.md +31 -1
  76. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/05-supported-resources.md +1 -1
  77. package/dist/docs/development/governance/architecture-change-detection/01-introduction.md +0 -10
  78. package/dist/docs/development/governance/architecture-change-detection/06-pipeline-gates.md +0 -4
  79. package/dist/docs/development/guides/97-architecture-graph.md +1 -1
  80. package/dist/docs/development/guides/99-adding-analytics.md +0 -7
  81. package/dist/docs/development/guides/domains/03-model-subdomains.md +2 -2
  82. package/dist/docs/development/guides/domains/04-add-resources-to-domains/01-add-systems-to-domains.md +3 -1
  83. package/dist/docs/development/guides/domains/04-add-resources-to-domains/02-add-services-to-domains.md +5 -1
  84. package/dist/docs/development/guides/domains/04-add-resources-to-domains/03-add-messages-to-domains.md +2 -2
  85. package/dist/docs/development/guides/domains/04-add-resources-to-domains/05-add-entities-to-domains.md +1 -1
  86. package/dist/docs/development/guides/domains/04-add-resources-to-domains/06-domain-resources-page.md +40 -0
  87. package/dist/docs/development/guides/domains/06-visualize-domains/01-domain-diagram.md +89 -0
  88. package/dist/docs/development/guides/domains/06-visualize-domains/_category_.json +1 -2
  89. package/dist/docs/development/guides/domains/09-reference.md +4 -4
  90. package/dist/docs/development/guides/owners/automated-teams-and-users/create-your-own-connector.md +0 -2
  91. package/dist/docs/development/guides/owners/automated-teams-and-users/sync-from-github.md +0 -3
  92. package/dist/docs/development/guides/owners/automated-teams-and-users/sync-from-microsoft-entra.md +0 -2
  93. package/dist/docs/development/guides/resources/adrs/03-linking-resources.md +8 -0
  94. package/dist/docs/development/guides/resources/adrs/06-reference.md +4 -4
  95. package/dist/docs/development/guides/resources/agents/11-reference.md +1 -1
  96. package/dist/docs/development/guides/resources/data/04-reference.md +1 -1
  97. package/dist/docs/development/guides/resources/data-products/07-reference.md +1 -1
  98. package/dist/docs/development/guides/resources/entities/05-entity-maps.md +1 -1
  99. package/dist/docs/development/guides/resources/entities/06-reference.md +2 -2
  100. package/dist/docs/development/guides/resources/flows/01-introduction.md +11 -1
  101. package/dist/docs/development/guides/resources/flows/03-model-flow-steps.md +65 -6
  102. package/dist/docs/development/guides/resources/flows/05-explore-flows.md +112 -0
  103. package/dist/docs/development/guides/resources/flows/06-reference.md +25 -4
  104. package/dist/docs/development/guides/resources/messages/01-what-are-messages.md +19 -0
  105. package/dist/docs/development/guides/resources/messages/04-message-channels/01-introduction.md +16 -0
  106. package/dist/docs/development/guides/resources/messages/04-message-channels/05-reference.md +2 -1
  107. package/dist/docs/development/guides/resources/messages/10-reference.md +6 -3
  108. package/dist/docs/development/guides/resources/schemas/04-explore-schemas/01-schema-explorer.md +2 -2
  109. package/dist/docs/development/guides/resources/schemas/04-explore-schemas/02-fields-explorer.md +0 -9
  110. package/dist/docs/development/guides/resources/schemas/05-schema-api.md +0 -3
  111. package/dist/docs/development/guides/resources/schemas/06-schema-mcp.md +11 -3
  112. package/dist/docs/development/guides/resources/services/04-add-resources-to-services/03-add-entities-to-services.md +1 -1
  113. package/dist/docs/development/guides/resources/services/05-resources-page.md +50 -0
  114. package/dist/docs/development/guides/resources/services/08-reference.md +2 -1
  115. package/dist/docs/development/guides/systems/01-introduction.md +1 -1
  116. package/dist/docs/development/guides/systems/02-create-system.md +2 -2
  117. package/dist/docs/development/guides/systems/03-add-resources-to-systems.md +35 -9
  118. package/dist/docs/development/guides/systems/04-model-relationships-and-actors.md +5 -3
  119. package/dist/docs/development/guides/systems/05-system-context-maps.md +39 -22
  120. package/dist/docs/development/guides/systems/06-reference.md +4 -4
  121. package/dist/docs/development/license/01-license-file.md +151 -0
  122. package/dist/docs/development/license/_category_.json +12 -0
  123. package/dist/docs/development/license-keys/_category_.json +3 -3
  124. package/dist/docs/development/license-keys/integrations.md +7 -37
  125. package/dist/docs/development/license-keys/license-validation.md +4 -0
  126. package/dist/docs/development/license-keys/overview.md +5 -1
  127. package/dist/docs/development/license-keys/plans.md +4 -2
  128. package/dist/docs/development/studio/01-introduction.md +60 -0
  129. package/dist/docs/development/studio/02-create-a-canvas.md +103 -0
  130. package/dist/docs/development/studio/03-set-up-persistent-storage.md +59 -0
  131. package/dist/docs/development/studio/04-collaborate-on-a-canvas.md +78 -0
  132. package/dist/docs/development/studio/05-connect-your-agent.md +102 -0
  133. package/dist/docs/development/studio/06-run-studio-in-production.md +54 -0
  134. package/dist/docs/development/studio/07-reference.md +294 -0
  135. package/dist/docs/development/studio/_category_.json +12 -0
  136. package/dist/docs/development/upgrading/v5.md +345 -0
  137. package/dist/docs/federation/00-overview.md +2 -8
  138. package/dist/docs/federation/01-first-federation.md +6 -18
  139. package/dist/docs/federation/explanation/how-it-works.md +0 -4
  140. package/dist/docs/federation/explanation/lockfile-and-cache.md +0 -4
  141. package/dist/docs/federation/explanation/mvp-status-and-feedback.md +0 -4
  142. package/dist/docs/federation/explanation/ownership-and-references.md +0 -4
  143. package/dist/docs/federation/how-to/configure-github-sources.md +6 -5
  144. package/dist/docs/federation/how-to/configure-validation-rules.md +0 -4
  145. package/dist/docs/federation/how-to/run-in-ci.md +2 -9
  146. package/dist/docs/federation/how-to/share-assets-and-components.md +0 -4
  147. package/dist/docs/federation/how-to/use-local-sources.md +0 -4
  148. package/dist/docs/federation/legacy-federation/00-introduction.md +4 -22
  149. package/dist/docs/federation/legacy-federation/01-installation.md +7 -58
  150. package/dist/docs/federation/legacy-federation/02-plugin-configuration.md +3 -21
  151. package/dist/docs/federation/legacy-federation/03-examples.md +3 -5
  152. package/dist/docs/federation/legacy-federation/04-configuration.md +3 -8
  153. package/dist/docs/federation/legacy-federation/05-setup-team-catalog.md +3 -6
  154. package/dist/docs/federation/reference/cli.md +2 -8
  155. package/dist/docs/federation/reference/configuration.md +0 -4
  156. package/dist/docs/federation/reference/diagnostic-rules.md +0 -5
  157. package/dist/docs/federation/reference/generated-output.md +0 -4
  158. package/dist/docs/federation/reference/supported-content.md +0 -4
  159. package/dist/docs/federation/reference/troubleshooting.md +0 -14
  160. package/dist/docs/plugins/02-generators.md +1 -1
  161. package/dist/docs/plugins/amazon-apigateway/00-intro.md +3 -28
  162. package/dist/docs/plugins/amazon-apigateway/01-installation.md +4 -56
  163. package/dist/docs/plugins/amazon-apigateway/02-plugin-configuration.md +1 -6
  164. package/dist/docs/plugins/apicurio/00-intro.md +2 -31
  165. package/dist/docs/plugins/apicurio/01-installation.md +2 -26
  166. package/dist/docs/plugins/apicurio/02-plugin-configuration.md +5 -13
  167. package/dist/docs/plugins/apicurio/03-features.md +1 -3
  168. package/dist/docs/plugins/asyncapi/00-intro.md +2 -28
  169. package/dist/docs/plugins/asyncapi/01-installation.md +3 -54
  170. package/dist/docs/plugins/asyncapi/02-plugin-configuration.md +2 -1
  171. package/dist/docs/plugins/aws-glue-registry/00-intro.md +4 -32
  172. package/dist/docs/plugins/aws-glue-registry/00a-installation.md +3 -53
  173. package/dist/docs/plugins/azure-schema-registry/00-intro.md +4 -32
  174. package/dist/docs/plugins/azure-schema-registry/01-installation.md +6 -33
  175. package/dist/docs/plugins/azure-schema-registry/02-plugin-configuration.md +1 -14
  176. package/dist/docs/plugins/azure-schema-registry/04-examples.md +0 -2
  177. package/dist/docs/plugins/backstage/00-overview.md +1 -7
  178. package/dist/docs/plugins/backstage/01-get-started.md +6 -15
  179. package/dist/docs/plugins/backstage/06-configuration.md +2 -8
  180. package/dist/docs/plugins/confluent-schema-registry/00-intro.md +2 -29
  181. package/dist/docs/plugins/confluent-schema-registry/01-installation.md +2 -26
  182. package/dist/docs/plugins/eventbridge/00-intro.md +0 -4
  183. package/dist/docs/plugins/eventbridge/00a-installation.md +1 -87
  184. package/dist/docs/plugins/eventbridge/03-api.md +2 -20
  185. package/dist/docs/plugins/github/00-intro.md +0 -31
  186. package/dist/docs/plugins/github/01-installation.md +2 -34
  187. package/dist/docs/plugins/graphql/00-intro.md +0 -30
  188. package/dist/docs/plugins/graphql/01-installation.md +1 -53
  189. package/dist/docs/plugins/graphql/02-plugin-configuration.md +0 -1
  190. package/dist/docs/plugins/hookdeck/01-intro.md +1 -1
  191. package/dist/docs/plugins/openapi/00-intro.md +0 -30
  192. package/dist/docs/plugins/openapi/01-installation.md +1 -57
  193. package/dist/docs/plugins/openapi/02-plugin-configuration.md +0 -5
  194. package/dist/docs/plugins/openapi/03-features.md +0 -4
  195. package/dist/eventcatalog-config-file-utils.cjs +34 -41
  196. package/dist/eventcatalog-config-file-utils.d.cts +56 -58
  197. package/dist/eventcatalog-config-file-utils.d.ts +56 -58
  198. package/dist/eventcatalog-config-file-utils.js +1 -1
  199. package/dist/eventcatalog.cjs +340 -249
  200. package/dist/eventcatalog.config.d.cts +18 -15
  201. package/dist/eventcatalog.config.d.ts +18 -15
  202. package/dist/eventcatalog.js +51 -33
  203. package/dist/features.cjs +36 -49
  204. package/dist/features.d.cts +1 -3
  205. package/dist/features.d.ts +1 -3
  206. package/dist/features.js +4 -6
  207. package/dist/federation/federate.cjs +37 -44
  208. package/dist/federation/federate.js +5 -5
  209. package/dist/federation/source-provider.js +2 -2
  210. package/dist/generate.cjs +37 -42
  211. package/dist/generate.js +4 -4
  212. package/dist/resolve-catalog-dependencies.cjs +34 -41
  213. package/dist/resolve-catalog-dependencies.js +2 -2
  214. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  215. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  216. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  217. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  218. package/dist/runtime-dependencies/manifest.json +5 -1
  219. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  220. package/dist/utils/child-process-signals.cjs +53 -0
  221. package/dist/utils/child-process-signals.d.cts +19 -0
  222. package/dist/utils/child-process-signals.d.ts +19 -0
  223. package/dist/utils/child-process-signals.js +6 -0
  224. package/dist/utils/cli-logger.cjs +1 -1
  225. package/dist/utils/cli-logger.js +2 -2
  226. package/dist/utils/license-status.cjs +11 -4
  227. package/dist/utils/license-status.d.cts +3 -2
  228. package/dist/utils/license-status.d.ts +3 -2
  229. package/dist/utils/license-status.js +1 -1
  230. package/eventcatalog/astro.config.mjs +21 -1
  231. package/eventcatalog/integrations/config-loader.mjs +56 -58
  232. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  233. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  234. package/eventcatalog/integrations/storage.mjs +20 -0
  235. package/eventcatalog/integrations/studio-server.mjs +4 -1
  236. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  237. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  238. package/eventcatalog/src/components/Homepage/CatalogDashboard.astro +342 -0
  239. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +60 -4
  240. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  241. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  242. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  243. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  244. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  245. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  246. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  247. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  248. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  249. package/eventcatalog/src/components/Seo.astro +5 -1
  250. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +53 -48
  251. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +183 -178
  252. package/eventcatalog/src/components/Settings/LicenseSettings.tsx +140 -31
  253. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +40 -35
  254. package/eventcatalog/src/components/Settings/LogoUpload.tsx +1 -1
  255. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +9 -8
  256. package/eventcatalog/src/components/Settings/Row.tsx +7 -4
  257. package/eventcatalog/src/components/Settings/SettingsShared.tsx +3 -3
  258. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
  259. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  260. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
  261. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  262. package/eventcatalog/src/components/TrialBadge.astro +20 -9
  263. package/eventcatalog/src/content.config.ts +19 -2
  264. package/eventcatalog/src/features/auth/error.astro +4 -2
  265. package/eventcatalog/src/features/auth/login.astro +2 -1
  266. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +21 -1
  267. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  268. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  269. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +13 -7
  270. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  271. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
  272. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
  273. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
  274. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +123 -98
  275. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +118 -101
  276. package/eventcatalog/src/features/mcp/mcp-server.ts +20 -4
  277. package/eventcatalog/src/features/server/start.ts +25 -0
  278. package/eventcatalog/src/features/storage/database.ts +88 -0
  279. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  280. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  281. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  282. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  283. package/eventcatalog/src/features/storage/sql.ts +28 -0
  284. package/eventcatalog/src/features/studio/agent-choreography.ts +13 -10
  285. package/eventcatalog/src/features/studio/agents.ts +28 -0
  286. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  287. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  288. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  289. package/eventcatalog/src/features/studio/canvas-actions.ts +160 -34
  290. package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
  291. package/eventcatalog/src/features/studio/catalog.ts +16 -14
  292. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +274 -105
  293. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +15 -14
  294. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
  295. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
  296. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  297. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  298. package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
  299. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +122 -230
  300. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  301. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  302. package/eventcatalog/src/features/studio/components/Presence.tsx +52 -45
  303. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
  304. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  305. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1314 -385
  306. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  307. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  308. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  309. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
  310. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
  311. package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
  312. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +8 -3
  313. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +244 -32
  314. package/eventcatalog/src/features/studio/identity.ts +25 -1
  315. package/eventcatalog/src/features/studio/limits.ts +2 -0
  316. package/eventcatalog/src/features/studio/pages/[id].astro +42 -16
  317. package/eventcatalog/src/features/studio/pages/index.astro +596 -373
  318. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +12 -15
  319. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +8 -23
  320. package/eventcatalog/src/features/studio/server/runtime.ts +150 -46
  321. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  322. package/eventcatalog/src/features/studio/server/storage.ts +53 -73
  323. package/eventcatalog/src/features/studio/webmcp-relay/LICENSE +21 -0
  324. package/eventcatalog/src/features/studio/webmcp-relay/README.md +5 -0
  325. package/eventcatalog/src/features/studio/webmcp-relay/embed.js +2 -0
  326. package/eventcatalog/src/features/studio/webmcp-relay/widget.html +10 -0
  327. package/eventcatalog/src/features/studio/webmcp-relay/widget.js +1 -0
  328. package/eventcatalog/src/features/tools/catalog-tools.ts +3 -1
  329. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  330. package/eventcatalog/src/layouts/SettingsLayout.astro +2 -2
  331. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +3 -2
  332. package/eventcatalog/src/pages/_index.astro +5 -342
  333. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +2 -3
  334. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +0 -7
  335. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -1
  336. package/eventcatalog/src/pages/diagrams/[id]/[version]/_index.data.ts +0 -7
  337. package/eventcatalog/src/pages/diagrams/[id]/[version]/embed.astro +1 -0
  338. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +1 -0
  339. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +5 -7
  340. package/eventcatalog/src/pages/directory/[type]/index.astro +3 -1
  341. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +5 -7
  342. package/eventcatalog/src/pages/discover/[type]/index.astro +3 -1
  343. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +0 -7
  344. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -0
  345. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +0 -7
  346. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +1 -0
  347. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +0 -7
  348. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +3 -1
  349. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/_[filename].data.ts +0 -7
  350. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +0 -7
  351. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -0
  352. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +1 -0
  353. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/_[...filename].data.ts +0 -4
  354. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +0 -7
  355. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/index.astro +1 -0
  356. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +3 -1
  357. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +0 -7
  358. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -0
  359. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +2 -3
  360. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +0 -7
  361. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +1 -0
  362. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +1 -0
  363. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_[filename].data.ts +0 -7
  364. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +0 -7
  365. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +2 -1
  366. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/_index.data.ts +0 -7
  367. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -0
  368. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +0 -7
  369. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +1 -0
  370. package/eventcatalog/src/pages/docs/custom/[...path]/_index.data.ts +0 -7
  371. package/eventcatalog/src/pages/docs/custom/[...path]/index.astro +1 -0
  372. package/eventcatalog/src/pages/docs/services/[id]/[docType]/[docId]/index.astro +1 -1
  373. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +1 -0
  374. package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename].astro +1 -1
  375. package/eventcatalog/src/pages/docs/services/[id]/changelog/index.astro +1 -1
  376. package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename].astro +1 -1
  377. package/eventcatalog/src/pages/docs/services/[id]/spec/[filename].astro +1 -1
  378. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +0 -7
  379. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -0
  380. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +0 -7
  381. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -0
  382. package/eventcatalog/src/pages/index.astro +4 -2
  383. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +0 -7
  384. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +9 -1
  385. package/eventcatalog/src/pages/schemas/explorer/_index.data.ts +0 -7
  386. package/eventcatalog/src/pages/schemas/explorer/index.astro +4 -2
  387. package/eventcatalog/src/pages/settings/license.astro +9 -1
  388. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +0 -7
  389. package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +1 -0
  390. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +0 -7
  391. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +0 -7
  392. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +1 -0
  393. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/_index.data.ts +0 -7
  394. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
  395. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
  396. package/eventcatalog/src/pages/visualiser/[type]/[id]/_index.data.ts +38 -0
  397. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +18 -41
  398. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +0 -7
  399. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -1
  400. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +0 -7
  401. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
  402. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
  403. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +0 -7
  404. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +1 -0
  405. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +0 -7
  406. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +1 -0
  407. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
  408. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  409. package/eventcatalog/src/utils/collections/flows.ts +20 -7
  410. package/eventcatalog/src/utils/collections/resource-owners.ts +8 -2
  411. package/eventcatalog/src/utils/collections/resources.ts +2 -0
  412. package/eventcatalog/src/utils/collections/util.ts +8 -4
  413. package/eventcatalog/src/utils/git.ts +11 -0
  414. package/eventcatalog/src/utils/homepage-dashboard.ts +143 -0
  415. package/eventcatalog/src/utils/license.ts +1 -1
  416. package/eventcatalog/src/utils/link-validation.ts +25 -19
  417. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  418. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  419. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +4 -2
  420. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  421. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  422. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  423. package/eventcatalog/src/utils/page-loaders/hybrid-page.ts +4 -15
  424. package/eventcatalog/src/utils/page-title.ts +15 -0
  425. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  426. package/eventcatalog/src/utils/trial.ts +13 -0
  427. package/package.json +11 -11
  428. package/dist/docs/development/ask-your-architecture/05-skills/02-installation.md +0 -60
  429. package/dist/docs/development/ask-your-architecture/05-skills/_category_.json +0 -11
  430. package/dist/docs/development/ask-your-architecture/06-slack-integration/01-introduction.md +0 -63
  431. package/dist/docs/development/ask-your-architecture/06-slack-integration/02-slack-app-setup.md +0 -154
  432. package/dist/docs/development/ask-your-architecture/06-slack-integration/03-installation.md +0 -169
  433. package/dist/docs/development/ask-your-architecture/06-slack-integration/04-deployment.md +0 -236
  434. package/dist/docs/development/ask-your-architecture/06-slack-integration/05-usage.md +0 -140
  435. package/dist/docs/development/ask-your-architecture/06-slack-integration/06-troubleshooting.md +0 -268
  436. package/dist/docs/development/ask-your-architecture/06-slack-integration/_category_.json +0 -13
  437. package/dist/docs/development/guides/domains/06-visualize-domains/01-domain-integration-map.md +0 -40
  438. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +0 -3
  439. package/eventcatalog/src/features/studio/canvas-doc.ts +0 -409
  440. package/eventcatalog/src/features/studio/clipboard.ts +0 -97
  441. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -154
  442. package/eventcatalog/src/features/studio/components/Comments.tsx +0 -770
  443. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
  444. package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
  445. package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
  446. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
  447. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -255
  448. package/eventcatalog/src/features/studio/components/icons.ts +0 -53
  449. package/eventcatalog/src/features/studio/components/status.ts +0 -51
  450. package/eventcatalog/src/features/studio/edges.ts +0 -56
  451. package/eventcatalog/src/features/studio/flow-state.ts +0 -92
  452. package/eventcatalog/src/features/studio/grouping.ts +0 -226
  453. package/eventcatalog/src/features/studio/layout.ts +0 -42
  454. package/eventcatalog/src/features/studio/levels.ts +0 -282
  455. package/eventcatalog/src/features/studio/node-types.ts +0 -205
  456. package/eventcatalog/src/features/studio/pages/new.astro +0 -24
  457. package/eventcatalog/src/features/studio/pages/wake.ts +0 -15
  458. package/eventcatalog/src/features/studio/placement.ts +0 -197
  459. package/eventcatalog/src/features/studio/studio.css +0 -273
  460. package/dist/{chunk-A2RZR3U4.js → chunk-6K7XZAYI.js} +3 -3
@@ -1,770 +0,0 @@
1
- import {
2
- createContext,
3
- memo,
4
- useCallback,
5
- useContext,
6
- useEffect,
7
- useRef,
8
- useState,
9
- type PointerEvent as ReactPointerEvent,
10
- type ReactNode,
11
- } from 'react';
12
- import { createPortal } from 'react-dom';
13
- import { useInternalNode, useReactFlow, useStore, ViewportPortal, type Node, type XYPosition } from '@xyflow/react';
14
- import { Bot, Check, Maximize2, ClipboardPaste, Copy, CopyPlus, MessageCircle, Pencil, RotateCcw, Trash2, X } from 'lucide-react';
15
- import { getAbsolutePosition } from '../grouping';
16
- import type { Author, CommentAnchor, Thread } from '../hooks/use-comments';
17
- import { isWrittenOnCanvas } from '../node-types';
18
- import { STATUS } from './status';
19
- import Picture from './Picture';
20
-
21
- const initials = (name: string) => name.slice(0, 2).toUpperCase();
22
-
23
- export const timeAgo = (timestamp: number) => {
24
- const seconds = Math.round((Date.now() - timestamp) / 1000);
25
- if (seconds < 60) return 'just now';
26
- const minutes = Math.round(seconds / 60);
27
- if (minutes < 60) return `${minutes}m ago`;
28
- const hours = Math.round(minutes / 60);
29
- if (hours < 24) return `${hours}h ago`;
30
- return new Date(timestamp).toLocaleDateString();
31
- };
32
-
33
- /** Where a pin sits on the canvas: on its node (so it moves with it), or where it was dropped */
34
- export const getAnchorPosition = (anchor: CommentAnchor, nodesById: Map<string, Node>): XYPosition => {
35
- const node = anchor.nodeId ? nodesById.get(anchor.nodeId) : undefined;
36
- if (!node || !anchor.offset) return anchor.position;
37
- // Nodes in containers are positioned relative to them
38
- const position = getAbsolutePosition(node, nodesById);
39
- return { x: position.x + anchor.offset.x, y: position.y + anchor.offset.y };
40
- };
41
-
42
- /** Pinned to a node, a comment keeps its offset from the node so it moves with it */
43
- export const anchorAt = (position: XYPosition, node?: Node, nodesById?: Map<string, Node>): CommentAnchor => {
44
- if (!node) return { position };
45
- const nodePosition = getAbsolutePosition(node, nodesById ?? new Map([[node.id, node]]));
46
- return { position, nodeId: node.id, offset: { x: position.x - nodePosition.x, y: position.y - nodePosition.y } };
47
- };
48
-
49
- const DRAG_THRESHOLD_PX = 4;
50
-
51
- const panelClass =
52
- 'nowheel nodrag nopan absolute z-40 w-72 rounded-xl border shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]';
53
-
54
- /** The people in a thread, in the order they joined it (the pin shows the first few, like Figma) */
55
- const participantsOf = (thread: Thread): Author[] => {
56
- const seen = new Map<string, Author>([[thread.author.name, thread.author]]);
57
- for (const message of thread.messages) if (!seen.has(message.author.name)) seen.set(message.author.name, message.author);
58
- return [...seen.values()];
59
- };
60
- const MAX_PIN_AVATARS = 3;
61
- /** How long the pointer rests on a pin before it opens its preview (passing over pins leaves them closed) */
62
- const PREVIEW_DELAY_MS = 500;
63
- const repliesLabel = (count: number) => (count === 1 ? '1 reply' : `${count} replies`);
64
-
65
- function Avatar({ author, muted, size = 'h-6 w-6' }: { author: Author; muted?: boolean; size?: string }) {
66
- return (
67
- <span
68
- title={author.name}
69
- className={`relative flex ${size} shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white ring-2 ring-[rgb(var(--ec-card-bg))]`}
70
- style={{ backgroundColor: muted ? 'rgb(var(--ec-page-text-muted))' : author.color }}
71
- >
72
- {author.agent ? <Bot size={13} /> : initials(author.name)}
73
- <Picture src={author.picture} className={`absolute inset-0 h-full w-full ${muted ? 'opacity-50 grayscale' : ''}`} />
74
- </span>
75
- );
76
- }
77
-
78
- /**
79
- * A comment pin, like Figma's: a speech bubble with the people in the thread, that opens into a preview of it
80
- * (who started it, when, what they said and how many replies) once the pointer rests on it. Click opens the
81
- * thread, dragging moves it: `onDrag` gets the screen point the pin's tip (its bottom left corner) is over,
82
- * keeping where on the pin it was grabbed.
83
- */
84
- function Pin({
85
- author,
86
- thread,
87
- active,
88
- selected,
89
- resolved,
90
- onClick,
91
- onDrag,
92
- }: {
93
- author: Author;
94
- /** The thread it's for (a new comment being written has none yet) */
95
- thread?: Thread;
96
- active?: boolean;
97
- /** Part of what's selected on the canvas (e.g. after selecting everything) */
98
- selected?: boolean;
99
- resolved?: boolean;
100
- onClick?: () => void;
101
- onDrag?: (tip: XYPosition, phase: 'move' | 'end') => void;
102
- }) {
103
- const drag = useRef<{ start: XYPosition; grab: XYPosition; moved: boolean } | null>(null);
104
- const [hovered, setHovered] = useState(false);
105
- const hoverTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
106
- useEffect(() => () => clearTimeout(hoverTimer.current), []);
107
- const [pressed, setPressed] = useState(false);
108
- const tipOf = (event: { clientX: number; clientY: number }) => ({
109
- x: event.clientX - drag.current!.grab.x,
110
- y: event.clientY - drag.current!.grab.y,
111
- });
112
- const people = thread ? participantsOf(thread) : [author];
113
- const [first, ...rest] = thread?.messages ?? [];
114
- // The preview shows while the pointer is over the pin, not while the thread is open or the pin is dragged
115
- const expanded = !!thread && !!first && hovered && !active && !pressed;
116
-
117
- return (
118
- <button
119
- onPointerEnter={() => {
120
- clearTimeout(hoverTimer.current);
121
- hoverTimer.current = setTimeout(() => setHovered(true), PREVIEW_DELAY_MS);
122
- }}
123
- onPointerLeave={() => {
124
- clearTimeout(hoverTimer.current);
125
- setHovered(false);
126
- }}
127
- onPointerDown={(event) => {
128
- if (event.button !== 0) return;
129
- // Keeps pointer events coming while the pointer is outside the pin (throws if the pointer has gone)
130
- try {
131
- event.currentTarget.setPointerCapture(event.pointerId);
132
- } catch {}
133
- setPressed(true);
134
- const rect = event.currentTarget.getBoundingClientRect();
135
- drag.current = {
136
- start: { x: event.clientX, y: event.clientY },
137
- grab: { x: event.clientX - rect.left, y: event.clientY - rect.bottom },
138
- moved: false,
139
- };
140
- }}
141
- onPointerMove={(event) => {
142
- const current = drag.current;
143
- if (!current || !onDrag) return;
144
- const distance = Math.hypot(event.clientX - current.start.x, event.clientY - current.start.y);
145
- if (!current.moved && distance < DRAG_THRESHOLD_PX) return;
146
- current.moved = true;
147
- onDrag(tipOf(event), 'move');
148
- }}
149
- onPointerUp={(event) => {
150
- const current = drag.current;
151
- setPressed(false);
152
- if (!current) return;
153
- if (current.moved) onDrag?.(tipOf(event), 'end');
154
- else onClick?.();
155
- drag.current = null;
156
- }}
157
- onPointerCancel={() => {
158
- drag.current = null;
159
- setPressed(false);
160
- }}
161
- // Keyboard activation (pointer clicks are handled on pointer up)
162
- onClick={(event) => event.detail === 0 && onClick?.()}
163
- aria-label={
164
- thread ? `Comment by ${thread.author.name}${rest.length ? `, ${repliesLabel(rest.length)}` : ''}` : 'New comment'
165
- }
166
- className={`block cursor-grab rounded-[18px] rounded-bl-none border p-1 text-left shadow-lg transition-[box-shadow,transform] active:cursor-grabbing bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] ${
167
- active ? 'ring-2 ring-[rgb(var(--ec-accent))]' : ''
168
- } ${expanded ? 'w-64' : ''}`}
169
- // Selected: a ring in the theme's button colour (strong in light and dark themes)
170
- style={selected ? { boxShadow: '0 0 0 2px rgb(var(--ec-card-bg)), 0 0 0 4px rgb(var(--ec-button-bg))' } : undefined}
171
- >
172
- {expanded ? (
173
- <span className="block space-y-1 px-1.5 py-1">
174
- <span className="flex items-center gap-2">
175
- <span className="flex -space-x-0.5">
176
- {people.slice(0, MAX_PIN_AVATARS).map((person) => (
177
- <Avatar key={person.name} author={person} muted={resolved} size="h-5 w-5" />
178
- ))}
179
- </span>
180
- <span className="min-w-0 truncate text-xs font-semibold">{thread.author.name}</span>
181
- <span className="shrink-0 text-[11px] text-[rgb(var(--ec-page-text-muted))]">{timeAgo(first.createdAt)}</span>
182
- </span>
183
- <span className="line-clamp-3 block whitespace-pre-wrap break-words text-xs">{first.text}</span>
184
- {(rest.length > 0 || resolved) && (
185
- <span className="flex items-center gap-2 text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))]">
186
- {rest.length > 0 && <span>{repliesLabel(rest.length)}</span>}
187
- {resolved && (
188
- <span className={`flex items-center gap-0.5 ${STATUS.success.text}`}>
189
- <Check size={11} /> Resolved
190
- </span>
191
- )}
192
- </span>
193
- )}
194
- </span>
195
- ) : (
196
- <span className="flex -space-x-1">
197
- {people.slice(0, MAX_PIN_AVATARS).map((person) => (
198
- <Avatar key={person.name} author={person} muted={resolved} />
199
- ))}
200
- {people.length > MAX_PIN_AVATARS && (
201
- <span className="flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-semibold ring-2 ring-[rgb(var(--ec-card-bg))] bg-[rgb(var(--ec-page-border))]">
202
- +{people.length - MAX_PIN_AVATARS}
203
- </span>
204
- )}
205
- </span>
206
- )}
207
- </button>
208
- );
209
- }
210
-
211
- function Composer({
212
- placeholder,
213
- onSubmit,
214
- onCancel,
215
- }: {
216
- placeholder: string;
217
- onSubmit: (text: string) => void;
218
- onCancel?: () => void;
219
- }) {
220
- const [text, setText] = useState('');
221
- const submit = () => {
222
- if (!text.trim()) return;
223
- onSubmit(text.trim());
224
- setText('');
225
- };
226
-
227
- return (
228
- <div className="flex items-end gap-2 p-2">
229
- <textarea
230
- autoFocus
231
- rows={1}
232
- value={text}
233
- placeholder={placeholder}
234
- onChange={(e) => setText(e.target.value)}
235
- onKeyDown={(e) => {
236
- if (e.key === 'Enter' && !e.shiftKey) {
237
- e.preventDefault();
238
- submit();
239
- }
240
- if (e.key === 'Escape') onCancel?.();
241
- }}
242
- className="max-h-32 min-h-[34px] flex-1 resize-none rounded-md border px-2 py-1.5 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
243
- />
244
- <button
245
- onClick={submit}
246
- disabled={!text.trim()}
247
- className="rounded-md px-2.5 py-1.5 text-xs font-medium disabled:opacity-40 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]"
248
- >
249
- Post
250
- </button>
251
- </div>
252
- );
253
- }
254
-
255
- function IconButton({ title, onClick, children }: { title: string; onClick: () => void; children: ReactNode }) {
256
- return (
257
- <button
258
- title={title}
259
- onClick={onClick}
260
- className="rounded p-1 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)] hover:text-[rgb(var(--ec-icon-hover))]"
261
- >
262
- {children}
263
- </button>
264
- );
265
- }
266
-
267
- /** Room kept between a thread's box and the edges of the canvas */
268
- const PANEL_MARGIN_PX = 12;
269
- /** A thread's box is at most this tall: longer threads scroll (or open bigger, in a dialog) */
270
- const PANEL_MAX_HEIGHT_PX = 360;
271
- /** What drags a thread's box around (its header) */
272
- const DragPanelContext = createContext<((event: ReactPointerEvent<HTMLElement>) => void) | null>(null);
273
-
274
- /**
275
- * The box a thread (or a new comment) opens in, beside its pin. It opens moved up as far as needed to be fully
276
- * on the canvas, and can be dragged around by its header to see more of the canvas or of a long thread. Where it's
277
- * been moved is only here, and goes when it closes.
278
- */
279
- function FloatingPanel({ children }: { children: ReactNode }) {
280
- const ref = useRef<HTMLDivElement>(null);
281
- const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 });
282
- const offsetRef = useRef(offset);
283
- offsetRef.current = offset;
284
-
285
- // Opened low on the canvas, or grown past the bottom of it (replies): moved up so its bottom (and the reply box)
286
- // can be seen
287
- useEffect(() => {
288
- const element = ref.current;
289
- if (!element) return;
290
- const fit = () => {
291
- const rect = element.getBoundingClientRect();
292
- const canvas = element.closest('.react-flow')?.getBoundingClientRect() ?? { top: 0, bottom: window.innerHeight };
293
- const up = Math.min(rect.bottom - (canvas.bottom - PANEL_MARGIN_PX), rect.top - (canvas.top + PANEL_MARGIN_PX));
294
- if (up > 0) setOffset((current) => ({ x: current.x, y: current.y - up }));
295
- };
296
- const observer = new ResizeObserver(fit);
297
- observer.observe(element);
298
- return () => observer.disconnect();
299
- }, []);
300
-
301
- // Pin and box are counter-scaled with the zoom, so the box moves by as much as the pointer does on screen
302
- const startDrag = useCallback((event: ReactPointerEvent<HTMLElement>) => {
303
- if (event.button !== 0 || (event.target as Element).closest('button')) return;
304
- event.preventDefault();
305
- const element = event.currentTarget;
306
- element.setPointerCapture(event.pointerId);
307
- const start = { x: event.clientX, y: event.clientY };
308
- const from = offsetRef.current;
309
- const move = (moveEvent: PointerEvent) =>
310
- setOffset({ x: from.x + moveEvent.clientX - start.x, y: from.y + moveEvent.clientY - start.y });
311
- const end = () => {
312
- element.removeEventListener('pointermove', move);
313
- element.removeEventListener('pointerup', end);
314
- element.removeEventListener('pointercancel', end);
315
- };
316
- element.addEventListener('pointermove', move);
317
- element.addEventListener('pointerup', end);
318
- element.addEventListener('pointercancel', end);
319
- }, []);
320
-
321
- return (
322
- <div
323
- ref={ref}
324
- className={`${panelClass} flex flex-col`}
325
- style={{
326
- left: `calc(100% + 8px + ${offset.x}px)`,
327
- top: offset.y,
328
- maxHeight: PANEL_MAX_HEIGHT_PX,
329
- }}
330
- >
331
- <DragPanelContext.Provider value={startDrag}>{children}</DragPanelContext.Provider>
332
- </div>
333
- );
334
- }
335
-
336
- function ThreadPopover({
337
- thread,
338
- onReply,
339
- onResolve,
340
- onDelete,
341
- onClose,
342
- onExpand,
343
- }: {
344
- thread: Thread;
345
- onReply: (text: string) => void;
346
- onResolve: (resolved: boolean) => void;
347
- onDelete: () => void;
348
- onClose: () => void;
349
- /** Open it bigger, in a dialog (not there when it's already in one) */
350
- onExpand?: () => void;
351
- }) {
352
- const listRef = useRef<HTMLDivElement>(null);
353
- const startDrag = useContext(DragPanelContext);
354
- useEffect(() => {
355
- listRef.current?.scrollTo({ top: listRef.current.scrollHeight });
356
- }, [thread.messages.length]);
357
-
358
- return (
359
- <>
360
- <div
361
- onPointerDown={startDrag ?? undefined}
362
- title={startDrag ? 'Drag to move' : undefined}
363
- className={`flex shrink-0 select-none items-center justify-between border-b px-3 py-2 border-[rgb(var(--ec-page-border))] ${
364
- startDrag ? 'cursor-move' : ''
365
- }`}
366
- >
367
- <span className="text-xs font-semibold">{thread.resolved ? 'Resolved thread' : 'Thread'}</span>
368
- <span className="flex items-center gap-0.5">
369
- <IconButton title={thread.resolved ? 'Reopen' : 'Resolve'} onClick={() => onResolve(!thread.resolved)}>
370
- {thread.resolved ? <RotateCcw size={14} /> : <Check size={14} />}
371
- </IconButton>
372
- <IconButton title="Delete thread" onClick={onDelete}>
373
- <Trash2 size={14} />
374
- </IconButton>
375
- {onExpand && (
376
- <IconButton title="Open bigger" onClick={onExpand}>
377
- <Maximize2 size={14} />
378
- </IconButton>
379
- )}
380
- <IconButton title="Close" onClick={onClose}>
381
- <X size={14} />
382
- </IconButton>
383
- </span>
384
- </div>
385
- <div ref={listRef} className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 py-3">
386
- {thread.messages.map((message) => (
387
- <div key={message.id} className="flex gap-2">
388
- <span
389
- className="relative flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
390
- style={{ backgroundColor: message.author.color }}
391
- >
392
- {message.author.agent ? <Bot size={13} /> : initials(message.author.name)}
393
- <Picture src={message.author.picture} />
394
- </span>
395
- <div className="min-w-0">
396
- <p className="text-xs">
397
- <span className="font-semibold">{message.author.name}</span>{' '}
398
- <span className="text-[rgb(var(--ec-page-text-muted))]">{timeAgo(message.createdAt)}</span>
399
- </p>
400
- <p className="whitespace-pre-wrap break-words text-sm">{message.text}</p>
401
- </div>
402
- </div>
403
- ))}
404
- </div>
405
- <div className="shrink-0 border-t border-[rgb(var(--ec-page-border))]">
406
- <Composer placeholder="Reply" onSubmit={onReply} onCancel={onClose} />
407
- </div>
408
- </>
409
- );
410
- }
411
-
412
- /** A thread opened bigger, in a dialog over the page (for long threads) */
413
- function ThreadDialog({
414
- thread,
415
- onReply,
416
- onResolve,
417
- onDelete,
418
- onShrink,
419
- onClose,
420
- }: {
421
- thread: Thread;
422
- onReply: (text: string) => void;
423
- onResolve: (resolved: boolean) => void;
424
- onDelete: () => void;
425
- /** Back to the box beside its pin */
426
- onShrink: () => void;
427
- onClose: () => void;
428
- }) {
429
- useEffect(() => {
430
- const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onShrink();
431
- window.addEventListener('keydown', onKeyDown);
432
- return () => window.removeEventListener('keydown', onKeyDown);
433
- }, [onShrink]);
434
- return (
435
- <div
436
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
437
- // It's in the canvas's React tree (a portal), so its events would reach the canvas: they stop here (clicks in
438
- // the dialog stop at it)
439
- onClick={(event) => {
440
- event.stopPropagation();
441
- onShrink();
442
- }}
443
- onPointerDown={(event) => event.stopPropagation()}
444
- onMouseDown={(event) => event.stopPropagation()}
445
- onDoubleClick={(event) => event.stopPropagation()}
446
- onContextMenu={(event) => event.stopPropagation()}
447
- onWheel={(event) => event.stopPropagation()}
448
- >
449
- <div
450
- role="dialog"
451
- aria-label="Comment thread"
452
- onClick={(event) => event.stopPropagation()}
453
- className="flex max-h-[80vh] w-full max-w-xl flex-col rounded-xl border shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
454
- >
455
- <ThreadPopover thread={thread} onReply={onReply} onResolve={onResolve} onDelete={onDelete} onClose={onClose} />
456
- </div>
457
- </div>
458
- );
459
- }
460
-
461
- /** Where a pin goes on the canvas, following only the node it's on (if it's on one), counter-scaled */
462
- function PinAt({ anchor, zoom, zIndex, children }: { anchor: CommentAnchor; zoom: number; zIndex: number; children: ReactNode }) {
463
- const node = useInternalNode(anchor.nodeId ?? '');
464
- const origin = anchor.nodeId && anchor.offset ? node?.internals.positionAbsolute : undefined;
465
- const { x, y } = origin && anchor.offset ? { x: origin.x + anchor.offset.x, y: origin.y + anchor.offset.y } : anchor.position;
466
- return (
467
- <div className="absolute" style={{ left: x, top: y, transform: `scale(${1 / zoom})`, transformOrigin: '0 0', zIndex }}>
468
- {/* The pin's tip (bottom left corner) sits on the anchor point, and it grows up and right as it opens */}
469
- <div className="nopan nodrag nowheel pointer-events-auto absolute bottom-0 left-0">{children}</div>
470
- </div>
471
- );
472
- }
473
-
474
- /**
475
- * Comment pins (and the open thread) on the canvas, in its own coordinates (React Flow's viewport portal) so
476
- * they move with it without re-rendering, counter-scaled to stay the same size on screen. Each pin follows
477
- * only its own node, so other nodes moving don't re-render them.
478
- */
479
- export const CommentLayer = memo(function CommentLayer({
480
- threads,
481
- author,
482
- openThreadId,
483
- draft,
484
- showResolved,
485
- onOpen,
486
- onCreate,
487
- onCancelDraft,
488
- onReply,
489
- onResolve,
490
- onDelete,
491
- onMove,
492
- onMoveDraft,
493
- selectedIds,
494
- selectionOffset,
495
- }: {
496
- threads: Thread[];
497
- author: Author;
498
- openThreadId: string | null;
499
- /** Threads that are part of what's selected */
500
- selectedIds: ReadonlySet<string>;
501
- /** How far what's selected has been dragged: selected pins not on a node move with it */
502
- selectionOffset: XYPosition | null;
503
- draft: CommentAnchor | null;
504
- showResolved: boolean;
505
- onOpen: (threadId: string | null) => void;
506
- onCreate: (text: string) => void;
507
- onCancelDraft: () => void;
508
- onReply: (threadId: string, text: string) => void;
509
- onResolve: (threadId: string, resolved: boolean) => void;
510
- onDelete: (threadId: string) => void;
511
- onMove: (threadId: string, anchor: CommentAnchor) => void;
512
- onMoveDraft: (anchor: CommentAnchor) => void;
513
- }) {
514
- const zoom = useStore((state) => state.transform[2]);
515
- const { screenToFlowPosition, getInternalNode } = useReactFlow();
516
- // A pin being dragged stays here until it's dropped, then moves for everyone
517
- const [moving, setMoving] = useState<{ threadId: string; anchor: CommentAnchor } | null>(null);
518
- // The open thread, shown bigger in a dialog
519
- const [expandedId, setExpandedId] = useState<string | null>(null);
520
- const expanded = expandedId && expandedId === openThreadId ? threads.find((thread) => thread.id === expandedId) : undefined;
521
-
522
- // While dragging, a pin follows the pointer. Dropped on a node it attaches to it (and moves with it),
523
- // anywhere else it stays on the canvas.
524
- const nodeUnder = (point: XYPosition) => {
525
- const element = document.elementsFromPoint(point.x, point.y).find((el) => el.closest('.react-flow__node'));
526
- const id = element?.closest('.react-flow__node')?.getAttribute('data-id');
527
- return id ? getInternalNode(id) : undefined;
528
- };
529
- const anchorFor = (tip: XYPosition, phase: 'move' | 'end'): CommentAnchor => {
530
- const position = screenToFlowPosition(tip);
531
- const node = phase === 'end' ? nodeUnder(tip) : undefined;
532
- if (!node) return { position };
533
- const origin = node.internals.positionAbsolute;
534
- return { position, nodeId: node.id, offset: { x: position.x - origin.x, y: position.y - origin.y } };
535
- };
536
-
537
- const visible = threads.filter((thread) => showResolved || !thread.resolved || thread.id === openThreadId);
538
-
539
- return (
540
- <ViewportPortal>
541
- {/* Above nodes (selected ones are raised to 1000) and other people's pointers */}
542
- <div className="absolute left-0 top-0" style={{ zIndex: 2001 }}>
543
- {visible.map((thread) => {
544
- const isOpen = thread.id === openThreadId;
545
- return (
546
- <PinAt
547
- key={thread.id}
548
- anchor={
549
- moving?.threadId === thread.id
550
- ? moving.anchor
551
- : selectionOffset && !thread.nodeId && selectedIds.has(thread.id)
552
- ? { position: { x: thread.position.x + selectionOffset.x, y: thread.position.y + selectionOffset.y } }
553
- : thread
554
- }
555
- zoom={zoom}
556
- zIndex={isOpen ? 2 : 1}
557
- >
558
- <Pin
559
- author={thread.author}
560
- thread={thread}
561
- active={isOpen}
562
- selected={selectedIds.has(thread.id)}
563
- resolved={thread.resolved}
564
- onClick={() => onOpen(isOpen ? null : thread.id)}
565
- onDrag={(tip, phase) => {
566
- if (phase === 'move') return setMoving({ threadId: thread.id, anchor: anchorFor(tip, phase) });
567
- onMove(thread.id, anchorFor(tip, phase));
568
- setMoving(null);
569
- }}
570
- />
571
- {/* (not while it's open bigger, in the dialog) */}
572
- {isOpen && expanded?.id !== thread.id && (
573
- <FloatingPanel>
574
- <ThreadPopover
575
- thread={thread}
576
- onReply={(text) => onReply(thread.id, text)}
577
- onResolve={(resolved) => onResolve(thread.id, resolved)}
578
- onDelete={() => onDelete(thread.id)}
579
- onClose={() => onOpen(null)}
580
- onExpand={() => setExpandedId(thread.id)}
581
- />
582
- </FloatingPanel>
583
- )}
584
- </PinAt>
585
- );
586
- })}
587
-
588
- {expanded &&
589
- createPortal(
590
- <ThreadDialog
591
- thread={expanded}
592
- onReply={(text) => onReply(expanded.id, text)}
593
- onResolve={(resolved) => onResolve(expanded.id, resolved)}
594
- onDelete={() => {
595
- setExpandedId(null);
596
- onDelete(expanded.id);
597
- }}
598
- onShrink={() => setExpandedId(null)}
599
- onClose={() => {
600
- setExpandedId(null);
601
- onOpen(null);
602
- }}
603
- />,
604
- document.body
605
- )}
606
-
607
- {draft && (
608
- <PinAt anchor={draft} zoom={zoom} zIndex={3}>
609
- <Pin author={author} active onDrag={(tip, phase) => onMoveDraft(anchorFor(tip, phase))} />
610
- <FloatingPanel>
611
- <Composer placeholder="Add a comment" onSubmit={onCreate} onCancel={onCancelDraft} />
612
- </FloatingPanel>
613
- </PinAt>
614
- )}
615
- </div>
616
- </ViewportPortal>
617
- );
618
- });
619
-
620
- export type ContextMenuState = { x: number; y: number; flowPosition: XYPosition; node?: Node } | null;
621
-
622
- export const CanvasContextMenu = memo(function CanvasContextMenu({
623
- menu,
624
- onAddComment,
625
- onEditDetails,
626
- onCopy,
627
- onDuplicate,
628
- onPaste,
629
- onDeleteNode,
630
- onClose,
631
- }: {
632
- menu: ContextMenuState;
633
- onAddComment: () => void;
634
- /** Open the node's details in the panel (not for notes, which are edited on the canvas) */
635
- onEditDetails: () => void;
636
- /** Copy the node (or the selection it's in) */
637
- onCopy: () => void;
638
- onDuplicate: () => void;
639
- /** Paste what was last copied where the menu was opened, if anything was */
640
- onPaste?: () => void;
641
- /** Delete the node (or the selection it's in) */
642
- onDeleteNode: () => void;
643
- onClose: () => void;
644
- }) {
645
- useEffect(() => {
646
- if (!menu) return;
647
- const close = (event: Event) => {
648
- if (event instanceof KeyboardEvent && event.key !== 'Escape') return;
649
- onClose();
650
- };
651
- window.addEventListener('pointerdown', close);
652
- window.addEventListener('keydown', close);
653
- window.addEventListener('wheel', close);
654
- return () => {
655
- window.removeEventListener('pointerdown', close);
656
- window.removeEventListener('keydown', close);
657
- window.removeEventListener('wheel', close);
658
- };
659
- }, [menu, onClose]);
660
-
661
- if (!menu) return null;
662
-
663
- const itemClass =
664
- 'flex w-full items-center gap-2 rounded px-2.5 py-1.5 text-left text-xs hover:bg-[rgb(var(--ec-page-border)/0.5)]';
665
- const shortcutClass = 'ml-auto text-[rgb(var(--ec-page-text-muted))]';
666
-
667
- return (
668
- <div
669
- // Keep the window pointerdown listener from closing the menu before a click lands
670
- onPointerDown={(e) => e.stopPropagation()}
671
- className="fixed z-50 w-44 rounded-lg border p-1 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
672
- style={{ left: menu.x, top: menu.y }}
673
- >
674
- <button className={itemClass} onClick={onAddComment}>
675
- <MessageCircle size={14} />
676
- Add comment
677
- <span className={shortcutClass}>C</span>
678
- </button>
679
- {menu.node && (
680
- <>
681
- {!isWrittenOnCanvas(menu.node.type) && (
682
- <button className={itemClass} onClick={onEditDetails}>
683
- <Pencil size={14} />
684
- Edit details
685
- </button>
686
- )}
687
- <button className={itemClass} onClick={onCopy}>
688
- <Copy size={14} />
689
- Copy
690
- <span className={shortcutClass}>⌘C</span>
691
- </button>
692
- <button className={itemClass} onClick={onDuplicate}>
693
- <CopyPlus size={14} />
694
- Duplicate
695
- <span className={shortcutClass}>⌘D</span>
696
- </button>
697
- <div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
698
- <button className={`${itemClass} ${STATUS.danger.text}`} onClick={onDeleteNode}>
699
- <Trash2 size={14} />
700
- Delete
701
- <span className={shortcutClass}>⌫</span>
702
- </button>
703
- </>
704
- )}
705
- {!menu.node && onPaste && (
706
- <button className={itemClass} onClick={onPaste}>
707
- <ClipboardPaste size={14} />
708
- Paste here
709
- <span className={shortcutClass}>⌘V</span>
710
- </button>
711
- )}
712
- </div>
713
- );
714
- });
715
-
716
- /** All threads, for the left panel. Clicking one opens it on the canvas. */
717
- export function CommentList({
718
- threads,
719
- showResolved,
720
- onToggleResolved,
721
- onOpen,
722
- }: {
723
- threads: Thread[];
724
- showResolved: boolean;
725
- onToggleResolved: () => void;
726
- onOpen: (thread: Thread) => void;
727
- }) {
728
- const shown = threads.filter((thread) => showResolved || !thread.resolved).reverse();
729
-
730
- return (
731
- <div className="flex min-h-0 flex-1 flex-col">
732
- <label className="mx-3 mb-2 flex items-center gap-2 text-xs text-[rgb(var(--ec-page-text-muted))]">
733
- <input type="checkbox" checked={showResolved} onChange={onToggleResolved} />
734
- Show resolved
735
- </label>
736
- <div className="min-h-0 flex-1 space-y-1.5 overflow-y-auto px-3 pb-3">
737
- {shown.length === 0 && (
738
- <p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">
739
- No comments yet. Right click the canvas, or press C, to add one.
740
- </p>
741
- )}
742
- {shown.map((thread) => {
743
- const [first, ...replies] = thread.messages;
744
- return (
745
- <button
746
- key={thread.id}
747
- onClick={() => onOpen(thread)}
748
- className={`w-full space-y-1 rounded-md border px-2.5 py-2 text-left bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))] ${
749
- thread.resolved ? 'opacity-60' : ''
750
- }`}
751
- >
752
- <p className="flex items-center gap-1.5 text-xs">
753
- <span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: thread.author.color }} />
754
- <span className="font-semibold">{thread.author.name}</span>
755
- <span className="text-[rgb(var(--ec-page-text-muted))]">{timeAgo(thread.createdAt)}</span>
756
- {thread.resolved && <Check size={12} className={`ml-auto ${STATUS.success.text}`} />}
757
- </p>
758
- <p className="line-clamp-2 text-xs">{first?.text}</p>
759
- {replies.length > 0 && (
760
- <p className="text-[11px] text-[rgb(var(--ec-page-text-muted))]">
761
- {replies.length} {replies.length === 1 ? 'reply' : 'replies'}
762
- </p>
763
- )}
764
- </button>
765
- );
766
- })}
767
- </div>
768
- </div>
769
- );
770
- }