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

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-RFIXKATC.js → chunk-4G7FSRPN.js} +35 -42
  21. package/dist/{chunk-6OC46WQJ.js → chunk-55QETQZ3.js} +66 -35
  22. package/dist/{chunk-TQLRG35T.js → chunk-AD7CL772.js} +78 -45
  23. package/dist/chunk-CA6WCQ2T.js +54 -0
  24. package/dist/chunk-CPW6G62V.js +17 -0
  25. package/dist/{chunk-423S2KHN.js → chunk-FLAZ2JVN.js} +4 -2
  26. package/dist/{chunk-BH2F5WPN.js → chunk-GQI5UYXZ.js} +1 -1
  27. package/dist/{chunk-GYIVJXOJ.js → chunk-NA7OTBWW.js} +1 -1
  28. package/dist/{chunk-MDWB5HLO.js → chunk-NKNOXQ62.js} +1 -1
  29. package/dist/{chunk-3PKS2IIP.js → chunk-OX6TSVHY.js} +1 -1
  30. package/dist/{chunk-36RYTYT5.js → chunk-PD7KTSYN.js} +1 -1
  31. package/dist/chunk-S5GWAUG5.js +29 -0
  32. package/dist/{chunk-NDJOAITI.js → chunk-T3OROMAU.js} +9 -3
  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,60 @@
1
+ import { useEffect, useRef, type ReactNode } from 'react';
2
+
3
+ /**
4
+ * Studio's dialogs: a card over the page, closed by Escape or clicking outside it (not while it's busy). Focus stays
5
+ * in it: anything that moves focus outside (e.g. the menu it was opened from, closing) sends it back to its first
6
+ * field or button.
7
+ */
8
+
9
+ export const SECONDARY_BUTTON =
10
+ 'rounded-lg border px-4 py-2 text-sm font-medium border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-page-border)/0.4)] disabled:opacity-50';
11
+ export const PRIMARY_BUTTON =
12
+ 'rounded-lg px-4 py-2 text-sm font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))] disabled:opacity-60';
13
+
14
+ export default function Dialog({
15
+ labelledBy,
16
+ role = 'dialog',
17
+ busy = false,
18
+ onClose,
19
+ children,
20
+ }: {
21
+ /** The id of its heading */
22
+ labelledBy: string;
23
+ /** `alertdialog` when it asks to confirm something that can't be undone */
24
+ role?: 'dialog' | 'alertdialog';
25
+ /** Doing what was asked: it can't be closed until it's done */
26
+ busy?: boolean;
27
+ onClose: () => void;
28
+ children: ReactNode;
29
+ }) {
30
+ useEffect(() => {
31
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && !busy && onClose();
32
+ window.addEventListener('keydown', onKeyDown);
33
+ return () => window.removeEventListener('keydown', onKeyDown);
34
+ }, [onClose, busy]);
35
+
36
+ const card = useRef<HTMLDivElement>(null);
37
+ useEffect(() => {
38
+ const keepFocus = (event: FocusEvent) => {
39
+ if (!card.current || card.current.contains(event.target as globalThis.Node)) return;
40
+ card.current.querySelector<HTMLElement>('input, button')?.focus();
41
+ };
42
+ document.addEventListener('focusin', keepFocus);
43
+ return () => document.removeEventListener('focusin', keepFocus);
44
+ }, []);
45
+
46
+ return (
47
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => !busy && onClose()}>
48
+ <div
49
+ ref={card}
50
+ role={role}
51
+ aria-modal="true"
52
+ aria-labelledby={labelledBy}
53
+ onClick={(event) => event.stopPropagation()}
54
+ className="w-full max-w-md rounded-xl border shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
55
+ >
56
+ {children}
57
+ </div>
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,109 @@
1
+ import { useState } from 'react';
2
+ import { Users } from 'lucide-react';
3
+ import { MAX_NAME_LENGTH, PERSON_COLORS } from '../identity';
4
+
5
+ const initials = (name: string) =>
6
+ name
7
+ .trim()
8
+ .split(/\s+/)
9
+ .map((word) => word[0])
10
+ .join('')
11
+ .slice(0, 2)
12
+ .toUpperCase();
13
+
14
+ /**
15
+ * Joining a canvas the first time (without signing in), as Studio on the web joins a live session: the name and colour
16
+ * you'll be seen by (your pointer, your avatar, your comments), picked before you start, with how you'll look to the
17
+ * others. Remembered in this browser for next time.
18
+ */
19
+ export default function JoinForm({
20
+ color,
21
+ onJoin,
22
+ }: {
23
+ /** The colour picked to start with (the one kept in this browser, if there is one) */
24
+ color?: string | null;
25
+ onJoin: (me: { name: string; color: string }) => void;
26
+ }) {
27
+ const [draft, setDraft] = useState('');
28
+ const [picked, setPicked] = useState(() => color ?? PERSON_COLORS[Math.floor(Math.random() * PERSON_COLORS.length)]);
29
+ const name = draft.trim();
30
+
31
+ return (
32
+ <div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/40 p-4">
33
+ <form
34
+ role="dialog"
35
+ aria-modal="true"
36
+ aria-labelledby="join-canvas-title"
37
+ onSubmit={(event) => {
38
+ event.preventDefault();
39
+ if (name) onJoin({ name, color: picked });
40
+ }}
41
+ className="w-full max-w-md rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-6 text-center text-[rgb(var(--ec-page-text))] shadow-2xl"
42
+ >
43
+ <span className="mx-auto flex h-11 w-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,rgb(var(--ec-accent))_12%,transparent)] text-[rgb(var(--ec-accent))]">
44
+ <Users aria-hidden className="h-5 w-5" />
45
+ </span>
46
+ <h1 id="join-canvas-title" className="mt-3 text-base font-semibold">
47
+ Join this canvas
48
+ </h1>
49
+ <p className="mt-1.5 text-sm leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
50
+ Everyone here sees your pointer and edits with you, live.
51
+ </p>
52
+
53
+ <label className="mt-5 block text-left text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
54
+ Your name
55
+ <input
56
+ autoFocus
57
+ value={draft}
58
+ maxLength={MAX_NAME_LENGTH}
59
+ placeholder="What should others call you?"
60
+ onChange={(event) => setDraft(event.target.value)}
61
+ className="mt-1.5 block h-9 w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 text-sm text-[rgb(var(--ec-page-text))] outline-none placeholder:text-[rgb(var(--ec-page-text-muted))] focus:border-[rgb(var(--ec-accent))]"
62
+ />
63
+ </label>
64
+ <div className="mt-4 text-left text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
65
+ Your colour
66
+ <div role="radiogroup" aria-label="Your colour" className="mt-2 flex gap-2">
67
+ {PERSON_COLORS.map((option) => (
68
+ <button
69
+ key={option}
70
+ type="button"
71
+ role="radio"
72
+ aria-checked={option === picked}
73
+ aria-label={option}
74
+ onClick={() => setPicked(option)}
75
+ className={`h-6 w-6 rounded-full transition-transform hover:scale-110 ${
76
+ option === picked
77
+ ? 'ring-2 ring-[rgb(var(--ec-page-text))] ring-offset-2 ring-offset-[rgb(var(--ec-card-bg))]'
78
+ : ''
79
+ }`}
80
+ style={{ background: option }}
81
+ />
82
+ ))}
83
+ </div>
84
+ </div>
85
+
86
+ {/* How you'll look to the others */}
87
+ {name && (
88
+ <div className="mt-5 flex items-center justify-center gap-2 text-sm">
89
+ <span
90
+ className="flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold text-white"
91
+ style={{ background: picked }}
92
+ >
93
+ {initials(name)}
94
+ </span>
95
+ <span className="font-medium">{name}</span>
96
+ </div>
97
+ )}
98
+
99
+ <button
100
+ type="submit"
101
+ disabled={!name}
102
+ className="mt-5 inline-flex h-10 w-full items-center justify-center rounded-lg bg-[rgb(var(--ec-page-text))] text-sm font-medium text-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-page-text)/0.85)] disabled:opacity-40"
103
+ >
104
+ Join canvas
105
+ </button>
106
+ </form>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,206 @@
1
+ import { memo, useMemo, useState } from 'react';
2
+ import { Check, Search } from 'lucide-react';
3
+ import type { CatalogResource } from '../catalog-resources';
4
+ import { catalogNodeData } from '../catalog';
5
+ import {
6
+ isShownOnLevel,
7
+ type Level,
8
+ type StartDrag,
9
+ CATALOG_GROUPS,
10
+ STATUS,
11
+ Palette,
12
+ PaletteItem,
13
+ PaletteSection,
14
+ type PaletteTab,
15
+ } from '@eventcatalog/visualiser/studio';
16
+
17
+ /** What's dragged from the left panel onto the canvas: a component's key, or a catalog resource's key */
18
+ export { COMPONENT_DRAG_TYPE } from '@eventcatalog/visualiser/studio';
19
+ export const CATALOG_DRAG_TYPE = 'application/eventcatalog-studio-resource';
20
+
21
+ /** A catalog group shows this many resources until it's expanded, so big catalogs stay quick */
22
+ const GROUP_PREVIEW_SIZE = 50;
23
+
24
+ export type LeftPanelTab = 'components' | 'catalog';
25
+
26
+ /**
27
+ * Studio's palette (as Studio on the web's): components to drag onto the canvas, and here also the catalog's
28
+ * resources, as a tab in the same card (the comments are listed from the bottom left controls)
29
+ */
30
+ export default memo(function LeftPanel({
31
+ level,
32
+ onShowAll,
33
+ resources,
34
+ keysOnCanvas,
35
+ onAddComponent,
36
+ onAddResource,
37
+ tab,
38
+ onTabChange,
39
+ startCollapsed,
40
+ }: {
41
+ /** The level shown: only what it shows is listed, so what's added stays on it */
42
+ level: Level;
43
+ /** Goes to L3, which shows everything */
44
+ onShowAll: () => void;
45
+ resources: CatalogResource[];
46
+ /** The catalog resources on the canvas */
47
+ keysOnCanvas: Set<string>;
48
+ onAddComponent: (type: string) => void;
49
+ onAddResource: (key: string) => void;
50
+ /** The tab shown, when the canvas chooses it */
51
+ tab?: LeftPanelTab;
52
+ onTabChange?: (tab: LeftPanelTab) => void;
53
+ /** Starts folded away (e.g. in a chat, where there's little room) */
54
+ startCollapsed?: boolean;
55
+ }) {
56
+ const shownResources = useMemo(
57
+ () => (level === 3 ? resources : resources.filter((resource) => isShownOnLevel(resource.node.type, level))),
58
+ [resources, level]
59
+ );
60
+ const tabs = useMemo(
61
+ (): PaletteTab[] => [
62
+ {
63
+ id: 'catalog',
64
+ label: 'Catalog Resources',
65
+ render: (startDrag) => (
66
+ <>
67
+ {level !== 3 && <LevelHint level={level} onShowAll={onShowAll} />}
68
+ <CatalogList resources={shownResources} keysOnCanvas={keysOnCanvas} onAdd={onAddResource} startDrag={startDrag} />
69
+ </>
70
+ ),
71
+ },
72
+ ],
73
+ [level, onShowAll, shownResources, keysOnCanvas, onAddResource]
74
+ );
75
+
76
+ return (
77
+ <Palette
78
+ level={level}
79
+ onAddComponent={onAddComponent}
80
+ tabs={tabs}
81
+ tab={tab}
82
+ onTabChange={onTabChange as ((tab: string) => void) | undefined}
83
+ startCollapsed={startCollapsed}
84
+ />
85
+ );
86
+ });
87
+
88
+ /** On L1 and L2: only what the level shows is listed (added there, it stays there), the rest is on L3 */
89
+ function LevelHint({ level, onShowAll }: { level: Level; onShowAll: () => void }) {
90
+ return (
91
+ <p className="text-[11px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
92
+ Showing what L{level} shows.{' '}
93
+ <button onClick={onShowAll} className="font-medium text-[rgb(var(--ec-accent))] hover:underline">
94
+ Go to L3 for everything
95
+ </button>
96
+ </p>
97
+ );
98
+ }
99
+
100
+ function CatalogList({
101
+ resources,
102
+ keysOnCanvas,
103
+ onAdd,
104
+ startDrag,
105
+ }: {
106
+ resources: CatalogResource[];
107
+ keysOnCanvas: Set<string>;
108
+ onAdd: (key: string) => void;
109
+ startDrag: StartDrag;
110
+ }) {
111
+ const [search, setSearch] = useState('');
112
+ const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
113
+ const groups = useMemo(() => {
114
+ const term = search.trim().toLowerCase();
115
+ const matches = term
116
+ ? resources.filter((resource) => resource.name.toLowerCase().includes(term) || resource.id.toLowerCase().includes(term))
117
+ : resources;
118
+ return CATALOG_GROUPS.map((group) => ({
119
+ ...group,
120
+ items: matches.filter((resource) => resource.collection === group.collection),
121
+ })).filter((group) => group.items.length > 0);
122
+ }, [resources, search]);
123
+
124
+ return (
125
+ <>
126
+ <div className="relative">
127
+ <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]" />
128
+ <input
129
+ // The list shows when its tab is chosen: typing then searches it, rather than reaching the canvas's shortcuts
130
+ // (from the tab button, typing "Order Service" picked the Comment tool)
131
+ autoFocus
132
+ value={search}
133
+ onChange={(e) => setSearch(e.target.value)}
134
+ placeholder="Search the catalog"
135
+ className="w-full rounded-lg border py-1.5 pl-8 pr-2 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
136
+ />
137
+ </div>
138
+ {groups.length === 0 && (
139
+ <p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
140
+ )}
141
+ {groups.map(({ collection, label, icon, className, items }) => {
142
+ const shown = expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
143
+ return (
144
+ <PaletteSection key={collection} title={`${label} (${items.length})`}>
145
+ {shown.map((resource) => (
146
+ <CatalogItem
147
+ key={resource.key}
148
+ resource={resource}
149
+ icon={icon}
150
+ iconClassName={className}
151
+ onCanvas={keysOnCanvas.has(resource.key)}
152
+ onAdd={onAdd}
153
+ startDrag={startDrag}
154
+ />
155
+ ))}
156
+ {shown.length < items.length && (
157
+ <button
158
+ onClick={() => setExpanded((current) => new Set(current).add(collection))}
159
+ className="w-full rounded-md px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
160
+ >
161
+ Show all {items.length} {label.toLowerCase()}
162
+ </button>
163
+ )}
164
+ </PaletteSection>
165
+ );
166
+ })}
167
+ </>
168
+ );
169
+ }
170
+
171
+ const CatalogItem = memo(function CatalogItem({
172
+ resource,
173
+ icon,
174
+ iconClassName,
175
+ onCanvas,
176
+ onAdd,
177
+ startDrag,
178
+ }: {
179
+ resource: CatalogResource;
180
+ icon: (typeof CATALOG_GROUPS)[number]['icon'];
181
+ iconClassName: string;
182
+ onCanvas: boolean;
183
+ onAdd: (key: string) => void;
184
+ startDrag: StartDrag;
185
+ }) {
186
+ // Drawn like the card it becomes (domains and systems can become containers too, but start as cards)
187
+ const preview = useMemo(() => ({ type: resource.node.type, data: catalogNodeData(resource) }), [resource]);
188
+ return (
189
+ <PaletteItem
190
+ icon={icon}
191
+ iconClassName={iconClassName}
192
+ label={resource.name}
193
+ description={resource.summary ? `v${resource.version} · ${resource.summary}` : `v${resource.version}`}
194
+ onDragStart={startDrag(CATALOG_DRAG_TYPE, resource.key, preview)}
195
+ onClick={() => onAdd(resource.key)}
196
+ title={`${resource.name}: drag onto the canvas, or click to add`}
197
+ trailing={
198
+ onCanvas ? (
199
+ <span title="On the canvas" className="flex-none">
200
+ <Check size={14} className={STATUS.success.text} />
201
+ </span>
202
+ ) : undefined
203
+ }
204
+ />
205
+ );
206
+ });
@@ -0,0 +1,183 @@
1
+ import { memo, type RefObject } from 'react';
2
+ import type { Node } from '@xyflow/react';
3
+ import { Box, ExternalLink, Library } from 'lucide-react';
4
+ import { getCatalogLink } from '../catalog';
5
+ import {
6
+ EditableText,
7
+ EditStatus,
8
+ Field,
9
+ NodeCard,
10
+ getNodeLabel,
11
+ getResource,
12
+ isVersioned,
13
+ updateResource,
14
+ useSavedFlash,
15
+ type UpdateNodeData,
16
+ NODE_ICONS,
17
+ } from '@eventcatalog/visualiser/studio';
18
+
19
+ /** Something it's connected to: the node at the other end, and the connection's label */
20
+ export type NodeConnection = { id: string; name: string; type?: string; label?: string; outgoing: boolean };
21
+
22
+ /**
23
+ * A node's details, in a card next to it (as Studio on the web's): its type and where it is, and its name, version
24
+ * and summary, saved as they're typed, and what it's connected to (click one to go to it). Catalog resources are read only (they're changed in the catalog), as is
25
+ * everything on a locked canvas. Notes and text are edited on the canvas.
26
+ */
27
+ export default memo(function NodeDetails({
28
+ node,
29
+ parentName,
30
+ connections,
31
+ onOpenNode,
32
+ containerRef,
33
+ locked,
34
+ onChange,
35
+ onClose,
36
+ }: {
37
+ /** What's shown of the node (not its position: the card follows the node on screen by itself) */
38
+ node: Pick<Node, 'id' | 'type' | 'data'>;
39
+ /** The domain or system it's in, if it's in one */
40
+ parentName?: string;
41
+ connections: NodeConnection[];
42
+ /** Goes to a node it's connected to (and shows its card) */
43
+ onOpenNode: (id: string) => void;
44
+ /** The canvas, to put the card next to the node */
45
+ containerRef: RefObject<HTMLElement | null>;
46
+ locked: boolean;
47
+ onChange: UpdateNodeData;
48
+ onClose: () => void;
49
+ }) {
50
+ const { saved, showSaved } = useSavedFlash();
51
+ const resource = getResource(node.type, node.data);
52
+ const link = getCatalogLink(node);
53
+ const label = getNodeLabel(node.type);
54
+ const { icon: Icon, className: iconClassName } = NODE_ICONS[node.type ?? ''] ?? { icon: Box, className: '' };
55
+ const versioned = isVersioned(node.type);
56
+ const name = String(resource.name ?? '');
57
+ const version = String(link?.version ?? resource.version ?? '');
58
+ const summary = String(resource.summary ?? '');
59
+ const save = (fields: Record<string, string>) => onChange(node.id, (data) => updateResource(node.type, data, fields));
60
+
61
+ return (
62
+ <NodeCard
63
+ nodeId={node.id}
64
+ containerRef={containerRef}
65
+ label={name || label}
66
+ onClose={onClose}
67
+ kicker={
68
+ <>
69
+ {/* Its type, in its colour (as the web's type pill) */}
70
+ <span className={`flex-none ${iconClassName}`}>
71
+ <span className="inline-flex items-center gap-1 rounded bg-current px-1.5 text-[9px] font-bold uppercase leading-4 tracking-wider">
72
+ <Icon aria-hidden size={10} className="text-white" />
73
+ <span className="text-white">{label}</span>
74
+ </span>
75
+ </span>
76
+ {parentName && <span className="ec-doc-card-breadcrumb">in {parentName}</span>}
77
+ </>
78
+ }
79
+ header={
80
+ link || locked ? (
81
+ <>
82
+ <h2 className="ec-doc-card-title">
83
+ {name}
84
+ {version && <span className="ec-doc-card-version">v{version}</span>}
85
+ </h2>
86
+ {summary && <p className="ec-doc-card-summary">{summary}</p>}
87
+ {link && (
88
+ <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-[rgb(var(--ec-page-text-muted))]">
89
+ <span className="inline-flex items-center gap-1.5">
90
+ <Library aria-hidden size={13} className="text-[rgb(var(--ec-accent))]" />
91
+ From your catalog: change it there
92
+ </span>
93
+ <a
94
+ href={link.url}
95
+ target="_blank"
96
+ rel="noreferrer"
97
+ className="inline-flex items-center gap-1 font-medium text-[rgb(var(--ec-accent))] hover:underline"
98
+ >
99
+ Open in EventCatalog <ExternalLink aria-hidden size={12} />
100
+ </a>
101
+ </div>
102
+ )}
103
+ </>
104
+ ) : (
105
+ // A small form: what's typed is saved as it's typed (there's nothing to press), and shown on the node
106
+ <div className="ec-edit-form" key={node.id}>
107
+ <Field label="Name">
108
+ <EditableText
109
+ label="Name"
110
+ value={name}
111
+ onSave={(next) => save({ name: next })}
112
+ onSaved={showSaved}
113
+ placeholder={`Name this ${label.toLowerCase()}`}
114
+ className="ec-editable-title"
115
+ required
116
+ />
117
+ </Field>
118
+ {versioned && (
119
+ <Field label="Version" className="ec-field-version">
120
+ <EditableText
121
+ label="Version"
122
+ value={version || '0.0.1'}
123
+ onSave={(next) => save({ version: next })}
124
+ onSaved={showSaved}
125
+ placeholder="0.0.1"
126
+ required
127
+ />
128
+ </Field>
129
+ )}
130
+ <Field label="Summary">
131
+ <EditableText
132
+ label="Summary"
133
+ value={summary}
134
+ onSave={(next) => save({ summary: next })}
135
+ onSaved={showSaved}
136
+ placeholder="What it does, in a sentence or two"
137
+ className="ec-editable-summary"
138
+ multiline
139
+ />
140
+ </Field>
141
+ <EditStatus saved={saved} />
142
+ </div>
143
+ )
144
+ }
145
+ >
146
+ <div className="ec-doc-card-body px-4 pb-3.5 pt-1 empty:hidden">
147
+ {(['Connects to', 'Connected from'] as const).map((heading) => {
148
+ const items = connections.filter((connection) => connection.outgoing === (heading === 'Connects to'));
149
+ if (!items.length) return null;
150
+ return (
151
+ <section key={heading} className="mt-3">
152
+ <h3 className="mb-1.5 text-xs font-semibold text-[rgb(var(--ec-page-text-muted))]">{heading}</h3>
153
+ <ul className="space-y-0.5">
154
+ {items.map((item) => {
155
+ const { icon: ItemIcon, className: itemIconClassName } = NODE_ICONS[item.type ?? ''] ?? {
156
+ icon: Box,
157
+ className: '',
158
+ };
159
+ return (
160
+ <li key={`${item.id}-${item.label}`}>
161
+ <button
162
+ type="button"
163
+ title={`Go to ${item.name}`}
164
+ onClick={() => onOpenNode(item.id)}
165
+ className="flex w-full items-center gap-2 rounded-md px-1.5 py-1 text-left text-sm hover:bg-[rgb(var(--ec-content-hover))]"
166
+ >
167
+ <ItemIcon aria-hidden size={15} className={`flex-none ${itemIconClassName}`} />
168
+ <span className="min-w-0 flex-1 truncate">{item.name}</span>
169
+ {item.label && (
170
+ <span className="flex-none text-[11px] text-[rgb(var(--ec-page-text-muted))]">{item.label}</span>
171
+ )}
172
+ </button>
173
+ </li>
174
+ );
175
+ })}
176
+ </ul>
177
+ </section>
178
+ );
179
+ })}
180
+ </div>
181
+ </NodeCard>
182
+ );
183
+ });
@@ -0,0 +1,79 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { MAX_TITLE_LENGTH } from '../limits';
3
+ import Dialog, { PRIMARY_BUTTON, SECONDARY_BUTTON } from './Dialog';
4
+ import { STATUS } from '@eventcatalog/visualiser/studio';
5
+
6
+ /**
7
+ * Naming the canvas a diagram is opened in (the visualiser's "Open in Studio"), named after the diagram to start
8
+ * with, like a new canvas on the Studio page. Loaded with the visualiser, so it imports nothing that loads a canvas.
9
+ */
10
+ export default function OpenInStudioDialog({
11
+ title,
12
+ onOpen,
13
+ onClose,
14
+ }: {
15
+ /** The diagram's title */
16
+ title?: string;
17
+ /** Makes the canvas with this name and opens it (rejects with what went wrong) */
18
+ onOpen: (title: string | undefined) => Promise<void>;
19
+ onClose: () => void;
20
+ }) {
21
+ const [name, setName] = useState((title ?? '').slice(0, MAX_TITLE_LENGTH));
22
+ const [opening, setOpening] = useState(false);
23
+ const [error, setError] = useState<string>();
24
+
25
+ const submit = async (event: FormEvent) => {
26
+ event.preventDefault();
27
+ setOpening(true);
28
+ setError(undefined);
29
+ try {
30
+ // Stays open (and busy) while the canvas page loads
31
+ await onOpen(name.trim() || undefined);
32
+ } catch (failure) {
33
+ setError((failure as Error).message);
34
+ setOpening(false);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <Dialog labelledBy="open-in-studio-title" busy={opening} onClose={onClose}>
40
+ <form onSubmit={submit}>
41
+ <div className="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
42
+ <h2 id="open-in-studio-title" className="text-base font-semibold">
43
+ Open in Studio
44
+ </h2>
45
+ <p className="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
46
+ A new canvas starts with this diagram. Name it for what you're designing, so your team can find it.
47
+ </p>
48
+ </div>
49
+ <div className="px-5 py-4">
50
+ <label htmlFor="open-in-studio-name" className="block text-sm font-semibold">
51
+ Name
52
+ </label>
53
+ <input
54
+ id="open-in-studio-name"
55
+ // Ready to type a new name, or keep the diagram's
56
+ autoFocus
57
+ onFocus={(event) => event.target.select()}
58
+ value={name}
59
+ onChange={(event) => setName(event.target.value)}
60
+ maxLength={MAX_TITLE_LENGTH}
61
+ autoComplete="off"
62
+ placeholder="e.g. Payments redesign"
63
+ disabled={opening}
64
+ className="mt-2 w-full rounded-lg border px-3 py-2 text-sm shadow-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:border-[rgb(var(--ec-accent))] focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.25)]"
65
+ />
66
+ {error && <p className={`mt-2 text-sm ${STATUS.danger.text}`}>{error}</p>}
67
+ </div>
68
+ <div className="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
69
+ <button type="button" onClick={onClose} disabled={opening} className={SECONDARY_BUTTON}>
70
+ Cancel
71
+ </button>
72
+ <button type="submit" disabled={opening} className={PRIMARY_BUTTON}>
73
+ {opening ? 'Opening…' : 'Open in Studio'}
74
+ </button>
75
+ </div>
76
+ </form>
77
+ </Dialog>
78
+ );
79
+ }