@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,10 +1,13 @@
1
1
  ---
2
2
  import path from 'node:path';
3
- import { ChevronDown, ChevronLeft, ChevronRight, Pencil, Plus, Search } from 'lucide-react';
3
+ import { ChevronDown, ChevronLeft, ChevronRight, Copy, EllipsisVertical, Pencil, Plus, Search, Trash2 } from 'lucide-react';
4
4
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
5
+ import StudioNotice from '../components/StudioNotice.astro';
5
6
  import { buildUrl } from '@utils/url-builder';
6
- import { CANVAS_STATUSES, type CanvasStatus } from '../canvas-doc';
7
- import { CANVAS_STATUS_LOOK } from '../components/status';
7
+ import { CANVAS_STATUSES, CANVAS_STATUS_LOOK, STATUS, type CanvasStatus } from '@eventcatalog/visualiser/studio';
8
+ import { MAX_TITLE_LENGTH } from '../limits';
9
+ import { MAX_NAME_LENGTH } from '../identity';
10
+ import { CANVASES_API_PATH } from '../api/canvases-api';
8
11
  import { startStudio, type CanvasSummary } from '../server/runtime';
9
12
  import { getSignedInUser, isSignedIn } from '../server/sign-in';
10
13
 
@@ -12,7 +15,12 @@ export const prerender = false;
12
15
 
13
16
  // The canvases on this EventCatalog, listed like the catalog's other tables (filters are in the address, so they can
14
17
  // be shared and work without JavaScript)
15
- const runtime = await startStudio({ isSignedIn });
18
+ // Studio can't start when its storage can't be used (e.g. the database was updated by a newer EventCatalog): this
19
+ // page says why instead of listing anything
20
+ const started = await startStudio({ isSignedIn }).catch((error: Error) => error);
21
+ const unavailable = started instanceof Error ? started.message : undefined;
22
+ if (unavailable) Astro.response.status = 503;
23
+ const runtime = started instanceof Error ? undefined : started;
16
24
  // Signed in (SSO): you're shown by who you signed in as, and asked for nothing
17
25
  const signedInAs = getSignedInUser(Astro.locals);
18
26
  const storage = runtime?.storage ?? { type: 'memory' as const };
@@ -22,13 +30,8 @@ const statusParam = Astro.url.searchParams.get('status');
22
30
  const status = CANVAS_STATUSES.includes(statusParam as CanvasStatus) ? (statusParam as CanvasStatus) : undefined;
23
31
  const search = Astro.url.searchParams.get('q')?.trim() ?? '';
24
32
 
25
- // Canvases started and left empty (opening a new one to look around) aren't listed
26
- const isBlank = (canvas: CanvasSummary) =>
27
- canvas.nodeCount === 0 && canvas.openComments === 0 && !canvas.title && canvas.people.length === 0;
28
- const canvases = (runtime?.listCanvases({ withPreview: true }) ?? [])
29
- .filter((canvas) => !isBlank(canvas))
30
- // The ones changed most recently first
31
- .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
33
+ // The ones changed most recently first (canvases started and left empty aren't listed)
34
+ const canvases = runtime?.listCanvases({ withPreview: true }) ?? [];
32
35
  const counts = Object.fromEntries(
33
36
  CANVAS_STATUSES.map((value) => [value, canvases.filter((canvas) => canvas.status === value).length])
34
37
  ) as Record<CanvasStatus, number>;
@@ -67,6 +70,9 @@ const ago = (time: number) => {
67
70
  if (Math.abs(days) < 30) return relativeTime.format(days, 'day');
68
71
  return new Date(time).toLocaleDateString('en', { day: 'numeric', month: 'short', year: 'numeric' });
69
72
  };
73
+ const canvasesApiUrl = buildUrl(CANVASES_API_PATH, true);
74
+ // Opens the New canvas dialog (also from a canvas's New canvas, or a link)
75
+ const newCanvasUrl = `${buildUrl('/studio')}?new=1`;
70
76
  const plural = (count: number, noun: string) => `${count} ${noun}${count === 1 ? '' : 's'}`;
71
77
 
72
78
  // Node colours in previews, like the visualiser's (by kind of node), from the theme's badge colours
@@ -95,384 +101,474 @@ const strokeOf = (preview: NonNullable<CanvasSummary['preview']>, ratio: number)
95
101
  Math.max(preview.width, preview.height) / ratio;
96
102
  ---
97
103
 
98
- <VerticalSideBarLayout title="Studio - EventCatalog" showNestedSideBar={false}>
99
- {/* The page's height, like Discover: the list scrolls between its header and its pages */}
100
- <main
101
- class="ml-0 flex flex-col bg-[rgb(var(--ec-page-bg))]"
102
- style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1); height: calc(100dvh - 60px);"
103
- >
104
- <div class="flex flex-shrink-0 flex-col gap-4 px-6 py-5 md:flex-row md:items-end md:justify-between">
105
- <div class="min-w-0">
106
- <h1 class="text-2xl font-semibold text-[rgb(var(--ec-page-text))] md:text-4xl">Studio ({canvases.length})</h1>
107
- <p class="max-w-3xl pt-2 text-base font-light text-[rgb(var(--ec-page-text-muted))]">
108
- Design changes to your architecture on live canvases, with your team and AI agents.
109
- </p>
110
- </div>
111
- <div class="flex shrink-0 flex-col items-start gap-3 md:items-end">
112
- {
113
- /* You, by the name others see on canvases: who you signed in as, or the name kept in this browser (shown by
114
- the script; its space is kept so nothing moves) */
115
- }
116
- {
117
- signedInAs ? (
118
- // Signed in: you're shown in the app's header already (still read by the script, for New canvas)
119
- <div data-you data-signed-in-as={signedInAs.name} hidden />
120
- ) : (
121
- <div data-you class="invisible flex items-center gap-2">
104
+ <VerticalSideBarLayout title="Studio" showNestedSideBar={false}>
105
+ {
106
+ unavailable ? (
107
+ <main
108
+ class="bg-[rgb(var(--ec-page-bg))]"
109
+ style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1); height: calc(100dvh - 60px);"
110
+ >
111
+ <StudioNotice title="Studio isn't available" message={unavailable} />
112
+ </main>
113
+ ) : (
114
+ <Fragment>
115
+ {/* The page's height, like Discover: the list scrolls between its header and its pages */}
116
+ <main
117
+ class="ml-0 flex flex-col bg-[rgb(var(--ec-page-bg))]"
118
+ style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1); height: calc(100dvh - 60px);"
119
+ >
120
+ <div class="flex flex-shrink-0 flex-col gap-4 px-6 py-5 md:flex-row md:items-end md:justify-between">
121
+ <div class="min-w-0">
122
+ <h1 class="text-2xl font-semibold text-[rgb(var(--ec-page-text))] md:text-4xl">Studio ({canvases.length})</h1>
123
+ <p class="max-w-3xl pt-2 text-base font-light text-[rgb(var(--ec-page-text-muted))]">
124
+ Design changes to your architecture on live canvases, with your team and AI agents.
125
+ </p>
126
+ </div>
127
+ <div class="flex shrink-0 flex-col items-start gap-3 md:items-end">
128
+ {/* You, by the name others see on canvases: who you signed in as, or the name kept in this browser (shown by
129
+ the script; its space is kept so nothing moves) */}
130
+ {signedInAs ? (
131
+ // Signed in: you're shown in the app's header already (still read by the script, for New canvas)
132
+ <div data-you data-signed-in-as={signedInAs.name} hidden />
133
+ ) : (
134
+ <div data-you class="invisible flex items-center gap-2">
135
+ <button
136
+ type="button"
137
+ title="Change your name"
138
+ class="group inline-flex max-w-48 items-center gap-2 rounded-lg border py-1 pl-1 pr-2.5 text-sm border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))]"
139
+ >
140
+ <span
141
+ data-you-avatar
142
+ class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
143
+ />
144
+ <span data-you-name class="truncate font-medium" />
145
+ <Pencil
146
+ size={12}
147
+ className="shrink-0 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-page-text))]"
148
+ />
149
+ </button>
150
+ </div>
151
+ )}
152
+ <div class="flex items-center gap-2">
153
+ <form method="get" action={buildUrl('/studio')} class="flex items-center gap-2" role="search">
154
+ {pageSize !== PAGE_SIZES[0] && <input type="hidden" name="size" value={pageSize} />}
155
+ <label class="relative">
156
+ <span class="sr-only">Filter canvases</span>
157
+ <Search
158
+ size={14}
159
+ className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
160
+ />
161
+ <input
162
+ type="search"
163
+ name="q"
164
+ value={search}
165
+ placeholder="Filter..."
166
+ class="w-56 rounded-lg border py-1.5 pl-8 pr-3 text-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))]"
167
+ />
168
+ </label>
169
+ <label class="relative">
170
+ <span class="sr-only">Status</span>
171
+ {/* Our own arrow: the browser's sits against the edge, whatever the padding */}
172
+ <select
173
+ name="status"
174
+ onchange="this.form.requestSubmit()"
175
+ class="cursor-pointer appearance-none rounded-lg border py-1.5 pl-3 pr-9 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))] focus:outline-none focus:border-[rgb(var(--ec-accent))]"
176
+ >
177
+ <option value="" selected={!status}>
178
+ All statuses
179
+ </option>
180
+ {CANVAS_STATUSES.map((value) => (
181
+ <option value={value} selected={status === value}>
182
+ {CANVAS_STATUS_LOOK[value].label} ({counts[value]})
183
+ </option>
184
+ ))}
185
+ </select>
186
+ <ChevronDown
187
+ size={14}
188
+ className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
189
+ />
190
+ </label>
191
+ </form>
192
+ <a
193
+ href={newCanvasUrl}
194
+ data-new-canvas
195
+ class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
196
+ >
197
+ <Plus size={14} />
198
+ New canvas
199
+ </a>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <div data-canvas-list class="min-h-0 flex-1 overflow-auto px-6 pb-5">
205
+ <div class="overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)]">
206
+ <table class="min-w-full divide-y divide-[rgb(var(--ec-page-border)/0.62)]">
207
+ <thead class="sticky top-0 z-10 bg-[rgb(var(--ec-content-hover)/0.45)]">
208
+ <tr>
209
+ {['Canvas', 'Status', 'Contents', 'Comments', 'Created', 'Updated'].map((label) => (
210
+ <th
211
+ scope="col"
212
+ class="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]"
213
+ >
214
+ {label}
215
+ </th>
216
+ ))}
217
+ <th scope="col" class="w-px px-4 py-2.5">
218
+ <span class="sr-only">Actions</span>
219
+ </th>
220
+ </tr>
221
+ </thead>
222
+ <tbody class="divide-y divide-[rgb(var(--ec-page-border)/0.5)]">
223
+ {onPage.map((canvas) => {
224
+ const look = CANVAS_STATUS_LOOK[canvas.status];
225
+ const preview = canvas.preview;
226
+ return (
227
+ <tr class="group relative transition-colors hover:bg-[rgb(var(--ec-content-hover)/0.38)]">
228
+ <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text))]">
229
+ <a href={buildUrl(`/studio/${canvas.canvasId}`)} class="flex items-center gap-3 font-semibold">
230
+ {/* The whole row opens it */}
231
+ <span class="absolute inset-0" aria-hidden="true" />
232
+ <span class="flex h-9 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))]">
233
+ {preview && (
234
+ <svg
235
+ viewBox={framed(preview)}
236
+ preserveAspectRatio="xMidYMid meet"
237
+ class="h-full w-full"
238
+ aria-hidden="true"
239
+ >
240
+ {preview.lines.map((line) => (
241
+ <line
242
+ {...line}
243
+ stroke="rgb(var(--ec-page-text-muted))"
244
+ stroke-opacity="0.5"
245
+ stroke-width={strokeOf(preview, 120)}
246
+ />
247
+ ))}
248
+ {preview.shapes.map((shape) => (
249
+ <rect
250
+ x={shape.x}
251
+ y={shape.y}
252
+ width={shape.width}
253
+ height={shape.height}
254
+ rx={strokeOf(preview, 40)}
255
+ fill={shape.container ? 'none' : colourOf(shape.type)}
256
+ fill-opacity="0.35"
257
+ stroke={shape.container ? 'rgb(var(--ec-page-text-muted))' : colourOf(shape.type)}
258
+ stroke-opacity={shape.container ? 0.5 : 1}
259
+ stroke-width={strokeOf(preview, shape.container ? 160 : 100)}
260
+ />
261
+ ))}
262
+ </svg>
263
+ )}
264
+ </span>
265
+ <span class="truncate group-hover:text-[rgb(var(--ec-accent))]">
266
+ {canvas.title ?? 'Untitled canvas'}
267
+ </span>
268
+ </a>
269
+ </td>
270
+ <td class="px-4 py-3 text-sm">
271
+ <span class:list={['inline-flex rounded-full border px-2 py-px text-[11px] font-medium', look.pill]}>
272
+ {look.label}
273
+ </span>
274
+ </td>
275
+ <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
276
+ {plural(canvas.nodeCount, 'node')}, {plural(canvas.edgeCount, 'connection')}
277
+ </td>
278
+ <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
279
+ {canvas.openComments > 0 ? `${canvas.openComments} open` : '-'}
280
+ </td>
281
+ <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
282
+ {[canvas.createdAt && ago(canvas.createdAt), canvas.createdBy && `by ${canvas.createdBy}`]
283
+ .filter(Boolean)
284
+ .join(' ') || '-'}
285
+ </td>
286
+ <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
287
+ {canvas.updatedAt ? ago(canvas.updatedAt) : '-'}
288
+ </td>
289
+ <td class="px-4 py-3 text-right">
290
+ {/* Above the link that covers the row */}
291
+ <button
292
+ type="button"
293
+ data-canvas-menu={canvas.canvasId}
294
+ data-canvas-title={canvas.title ?? ''}
295
+ title="More"
296
+ aria-label={`More for ${canvas.title ?? 'Untitled canvas'}`}
297
+ aria-haspopup="menu"
298
+ aria-expanded="false"
299
+ class="relative z-[1] rounded-md p-1.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)] hover:text-[rgb(var(--ec-page-text))] aria-expanded:bg-[rgb(var(--ec-page-border)/0.5)]"
300
+ >
301
+ <EllipsisVertical size={16} />
302
+ </button>
303
+ </td>
304
+ </tr>
305
+ );
306
+ })}
307
+ {shown.length === 0 && (
308
+ <tr>
309
+ <td colspan="7" class="px-4 py-12 text-center">
310
+ <p class="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">
311
+ {canvases.length === 0 ? 'No canvases yet' : 'No canvases match'}
312
+ </p>
313
+ <p class="mt-1 text-xs text-[rgb(var(--ec-icon-color))]">
314
+ {canvases.length === 0 ? (
315
+ <a href={newCanvasUrl} data-new-canvas class="text-[rgb(var(--ec-accent))] hover:underline">
316
+ Start a canvas
317
+ </a>
318
+ ) : (
319
+ <a href={buildUrl('/studio')} class="text-[rgb(var(--ec-accent))] hover:underline">
320
+ Show all canvases
321
+ </a>
322
+ )}
323
+ </p>
324
+ </td>
325
+ </tr>
326
+ )}
327
+ </tbody>
328
+ </table>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="flex flex-shrink-0 items-center justify-between gap-4 border-t border-[rgb(var(--ec-page-border))] px-6 py-3">
333
+ <div class="flex min-w-0 items-center gap-3 text-xs text-[rgb(var(--ec-page-text-muted))]">
334
+ {shown.length > 0 && (
335
+ <span class="shrink-0">
336
+ <span class="font-medium text-[rgb(var(--ec-page-text))]">{onPage.length}</span> of{' '}
337
+ <span class="font-medium text-[rgb(var(--ec-page-text))]">{shown.length}</span> results
338
+ </span>
339
+ )}
340
+ {shown.length > 0 && <span aria-hidden="true" class="h-3 w-px shrink-0 bg-[rgb(var(--ec-page-border))]" />}
341
+ <form method="get" action={buildUrl('/studio')} class="flex shrink-0 items-center gap-1.5">
342
+ {search && <input type="hidden" name="q" value={search} />}
343
+ {status && <input type="hidden" name="status" value={status} />}
344
+ <label for="studio-page-size">Per page</label>
345
+ <select
346
+ id="studio-page-size"
347
+ name="size"
348
+ onchange="this.form.requestSubmit()"
349
+ class="cursor-pointer rounded bg-transparent px-1 py-0.5 font-medium text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))] focus:bg-[rgb(var(--ec-content-hover))] focus:outline-none"
350
+ >
351
+ {PAGE_SIZES.map((size) => (
352
+ <option value={size} selected={size === pageSize}>
353
+ {size}
354
+ </option>
355
+ ))}
356
+ </select>
357
+ </form>
358
+ <span aria-hidden="true" class="h-3 w-px shrink-0 bg-[rgb(var(--ec-page-border))]" />
359
+ <span class="truncate">
360
+ {storage.type === 'memory'
361
+ ? 'Kept in memory until EventCatalog restarts (set storage in eventcatalog.config.js to keep them)'
362
+ : `Saved in ${savedIn}`}
363
+ </span>
364
+ </div>
365
+ <nav class="flex shrink-0 items-center gap-1.5" aria-label="Pages">
366
+ {pageNumber > 1 ? (
367
+ <a
368
+ href={pageUrl(pageNumber - 1)}
369
+ title="Previous page"
370
+ class="p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:text-[rgb(var(--ec-page-text))]"
371
+ >
372
+ <ChevronLeft className="h-4 w-4" />
373
+ </a>
374
+ ) : (
375
+ <span aria-disabled="true" class="p-1.5 text-[rgb(var(--ec-icon-color))] opacity-30">
376
+ <ChevronLeft className="h-4 w-4" />
377
+ </span>
378
+ )}
379
+ <span class="min-w-[60px] text-center text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))]">
380
+ <span class="font-medium text-[rgb(var(--ec-page-text))]">{pageNumber}</span> / {pageCount}
381
+ </span>
382
+ {pageNumber < pageCount ? (
383
+ <a
384
+ href={pageUrl(pageNumber + 1)}
385
+ title="Next page"
386
+ class="p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:text-[rgb(var(--ec-page-text))]"
387
+ >
388
+ <ChevronRight className="h-4 w-4" />
389
+ </a>
390
+ ) : (
391
+ <span aria-disabled="true" class="p-1.5 text-[rgb(var(--ec-icon-color))] opacity-30">
392
+ <ChevronRight className="h-4 w-4" />
393
+ </span>
394
+ )}
395
+ </nav>
396
+ </div>
397
+ </main>
398
+
399
+ {/* Naming a new canvas before it opens (created through the Studio API) */}
400
+ <dialog
401
+ id="new-canvas-dialog"
402
+ aria-labelledby="new-canvas-title"
403
+ class="m-auto w-full max-w-md rounded-xl border p-0 shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] backdrop:bg-black/40"
404
+ >
405
+ <form data-api={canvasesApiUrl}>
406
+ <div class="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
407
+ <h2 id="new-canvas-title" class="text-base font-semibold">
408
+ New canvas
409
+ </h2>
410
+ <p class="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
411
+ Name it for what you're designing, so your team can find it.
412
+ </p>
413
+ </div>
414
+ <div class="px-5 py-4">
415
+ <label for="new-canvas-name" class="block text-sm font-semibold">
416
+ Name
417
+ </label>
418
+ <input
419
+ id="new-canvas-name"
420
+ name="title"
421
+ maxlength={MAX_TITLE_LENGTH}
422
+ autocomplete="off"
423
+ placeholder="e.g. Payments redesign"
424
+ class="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)]"
425
+ />
426
+ {/* Who's starting it: the name they use on canvases, if they've joined one in this browser */}
427
+ <input type="hidden" name="by" />
428
+ <p data-error hidden class:list={['mt-2 text-sm', STATUS.danger.text]} />
429
+ </div>
430
+ <div class="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
122
431
  <button
123
432
  type="button"
124
- title="Change your name"
125
- class="group inline-flex max-w-48 items-center gap-2 rounded-lg border py-1 pl-1 pr-2.5 text-sm border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))]"
433
+ data-close-dialog
434
+ class="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)]"
126
435
  >
127
- <span
128
- data-you-avatar
129
- class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
130
- />
131
- <span data-you-name class="truncate font-medium" />
132
- <Pencil
133
- size={12}
134
- className="shrink-0 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-page-text))]"
135
- />
436
+ Cancel
136
437
  </button>
137
- </div>
138
- )
139
- }
140
- <div class="flex items-center gap-2">
141
- <form method="get" action={buildUrl('/studio')} class="flex items-center gap-2" role="search">
142
- {pageSize !== PAGE_SIZES[0] && <input type="hidden" name="size" value={pageSize} />}
143
- <label class="relative">
144
- <span class="sr-only">Filter canvases</span>
145
- <Search
146
- size={14}
147
- className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
148
- />
149
- <input
150
- type="search"
151
- name="q"
152
- value={search}
153
- placeholder="Filter..."
154
- class="w-56 rounded-lg border py-1.5 pl-8 pr-3 text-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))]"
155
- />
156
- </label>
157
- <label class="relative">
158
- <span class="sr-only">Status</span>
159
- {/* Our own arrow: the browser's sits against the edge, whatever the padding */}
160
- <select
161
- name="status"
162
- onchange="this.form.requestSubmit()"
163
- class="cursor-pointer appearance-none rounded-lg border py-1.5 pl-3 pr-9 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))] focus:outline-none focus:border-[rgb(var(--ec-accent))]"
438
+ <button
439
+ type="submit"
440
+ class="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"
164
441
  >
165
- <option value="" selected={!status}>All statuses</option>
166
- {
167
- CANVAS_STATUSES.map((value) => (
168
- <option value={value} selected={status === value}>
169
- {CANVAS_STATUS_LOOK[value].label} ({counts[value]})
170
- </option>
171
- ))
172
- }
173
- </select>
174
- <ChevronDown
175
- size={14}
176
- className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
177
- />
178
- </label>
442
+ Create canvas
443
+ </button>
444
+ </div>
179
445
  </form>
180
- <a
181
- href={buildUrl('/studio/new')}
182
- data-new-canvas
183
- class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
446
+ </dialog>
447
+
448
+ {/* A canvas's menu, opened from its row (one for the page, moved to the row it's opened from) */}
449
+ <div
450
+ id="canvas-row-menu"
451
+ role="menu"
452
+ hidden
453
+ data-api={canvasesApiUrl}
454
+ class="fixed z-50 min-w-44 rounded-xl border p-1 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
455
+ >
456
+ <button
457
+ type="button"
458
+ role="menuitem"
459
+ data-menu-copy
460
+ class="flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs hover:bg-[rgb(var(--ec-page-border)/0.5)] disabled:opacity-60"
461
+ >
462
+ <Copy size={14} />
463
+ <span data-label>Make a copy</span>
464
+ </button>
465
+ <div class="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
466
+ <button
467
+ type="button"
468
+ role="menuitem"
469
+ data-menu-delete
470
+ class:list={[
471
+ 'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs',
472
+ STATUS.danger.text,
473
+ STATUS.danger.hover,
474
+ ]}
184
475
  >
185
- <Plus size={14} />
186
- New canvas
187
- </a>
476
+ <Trash2 size={14} />
477
+ Delete
478
+ </button>
479
+ <p data-error hidden class:list={['max-w-56 px-2.5 py-1.5 text-xs', STATUS.danger.text]} />
188
480
  </div>
189
- </div>
190
- </div>
191
-
192
- <div class="min-h-0 flex-1 overflow-auto px-6 pb-5">
193
- <div
194
- class="overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)]"
195
- >
196
- <table class="min-w-full divide-y divide-[rgb(var(--ec-page-border)/0.62)]">
197
- <thead class="sticky top-0 z-10 bg-[rgb(var(--ec-content-hover)/0.45)]">
198
- <tr>
199
- {
200
- ['Canvas', 'Status', 'Contents', 'Comments', 'Created', 'Updated'].map((label) => (
201
- <th
202
- scope="col"
203
- class="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]"
204
- >
205
- {label}
206
- </th>
207
- ))
208
- }
209
- </tr>
210
- </thead>
211
- <tbody class="divide-y divide-[rgb(var(--ec-page-border)/0.5)]">
212
- {
213
- onPage.map((canvas) => {
214
- const look = CANVAS_STATUS_LOOK[canvas.status];
215
- const preview = canvas.preview;
216
- return (
217
- <tr class="group relative transition-colors hover:bg-[rgb(var(--ec-content-hover)/0.38)]">
218
- <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text))]">
219
- <a href={buildUrl(`/studio/${canvas.canvasId}`)} class="flex items-center gap-3 font-semibold">
220
- {/* The whole row opens it */}
221
- <span class="absolute inset-0" aria-hidden="true" />
222
- <span class="flex h-9 w-16 shrink-0 items-center justify-center overflow-hidden rounded-md border bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))]">
223
- {preview && (
224
- <svg
225
- viewBox={framed(preview)}
226
- preserveAspectRatio="xMidYMid meet"
227
- class="h-full w-full"
228
- aria-hidden="true"
229
- >
230
- {preview.lines.map((line) => (
231
- <line
232
- {...line}
233
- stroke="rgb(var(--ec-page-text-muted))"
234
- stroke-opacity="0.5"
235
- stroke-width={strokeOf(preview, 120)}
236
- />
237
- ))}
238
- {preview.shapes.map((shape) => (
239
- <rect
240
- x={shape.x}
241
- y={shape.y}
242
- width={shape.width}
243
- height={shape.height}
244
- rx={strokeOf(preview, 40)}
245
- fill={shape.container ? 'none' : colourOf(shape.type)}
246
- fill-opacity="0.35"
247
- stroke={shape.container ? 'rgb(var(--ec-page-text-muted))' : colourOf(shape.type)}
248
- stroke-opacity={shape.container ? 0.5 : 1}
249
- stroke-width={strokeOf(preview, shape.container ? 160 : 100)}
250
- />
251
- ))}
252
- </svg>
253
- )}
254
- </span>
255
- <span class="truncate group-hover:text-[rgb(var(--ec-accent))]">{canvas.title ?? 'Untitled canvas'}</span>
256
- </a>
257
- </td>
258
- <td class="px-4 py-3 text-sm">
259
- <span class:list={['inline-flex rounded-full border px-2 py-px text-[11px] font-medium', look.pill]}>
260
- {look.label}
261
- </span>
262
- </td>
263
- <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
264
- {plural(canvas.nodeCount, 'node')}, {plural(canvas.edgeCount, 'connection')}
265
- </td>
266
- <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
267
- {canvas.openComments > 0 ? `${canvas.openComments} open` : '-'}
268
- </td>
269
- <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
270
- {[canvas.createdAt && ago(canvas.createdAt), canvas.createdBy && `by ${canvas.createdBy}`]
271
- .filter(Boolean)
272
- .join(' ') || '-'}
273
- </td>
274
- <td class="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
275
- {canvas.updatedAt ? ago(canvas.updatedAt) : '-'}
276
- </td>
277
- </tr>
278
- );
279
- })
280
- }
281
- {
282
- shown.length === 0 && (
283
- <tr>
284
- <td colspan="6" class="px-4 py-12 text-center">
285
- <p class="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">
286
- {canvases.length === 0 ? 'No canvases yet' : 'No canvases match'}
287
- </p>
288
- <p class="mt-1 text-xs text-[rgb(var(--ec-icon-color))]">
289
- {canvases.length === 0 ? (
290
- <a href={buildUrl('/studio/new')} data-new-canvas class="text-[rgb(var(--ec-accent))] hover:underline">
291
- Start a canvas
292
- </a>
293
- ) : (
294
- <a href={buildUrl('/studio')} class="text-[rgb(var(--ec-accent))] hover:underline">
295
- Show all canvases
296
- </a>
297
- )}
298
- </p>
299
- </td>
300
- </tr>
301
- )
302
- }
303
- </tbody>
304
- </table>
305
- </div>
306
- </div>
307
481
 
308
- <div class="flex flex-shrink-0 items-center justify-between gap-4 border-t border-[rgb(var(--ec-page-border))] px-6 py-3">
309
- <div class="flex min-w-0 items-center gap-3 text-xs text-[rgb(var(--ec-page-text-muted))]">
310
- {
311
- shown.length > 0 && (
312
- <span class="shrink-0">
313
- <span class="font-medium text-[rgb(var(--ec-page-text))]">{onPage.length}</span> of{' '}
314
- <span class="font-medium text-[rgb(var(--ec-page-text))]">{shown.length}</span> results
315
- </span>
316
- )
317
- }
318
- {shown.length > 0 && <span aria-hidden="true" class="h-3 w-px shrink-0 bg-[rgb(var(--ec-page-border))]" />}
319
- <form method="get" action={buildUrl('/studio')} class="flex shrink-0 items-center gap-1.5">
320
- {search && <input type="hidden" name="q" value={search} />}
321
- {status && <input type="hidden" name="status" value={status} />}
322
- <label for="studio-page-size">Per page</label>
323
- <select
324
- id="studio-page-size"
325
- name="size"
326
- onchange="this.form.requestSubmit()"
327
- class="cursor-pointer rounded bg-transparent px-1 py-0.5 font-medium text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))] focus:bg-[rgb(var(--ec-content-hover))] focus:outline-none"
328
- >
329
- {
330
- PAGE_SIZES.map((size) => (
331
- <option value={size} selected={size === pageSize}>
332
- {size}
333
- </option>
334
- ))
335
- }
336
- </select>
337
- </form>
338
- <span aria-hidden="true" class="h-3 w-px shrink-0 bg-[rgb(var(--ec-page-border))]"></span>
339
- <span class="truncate">
340
- {
341
- storage.type === 'memory'
342
- ? 'Kept in memory until EventCatalog restarts (set studio.storage in eventcatalog.config.js to keep them)'
343
- : `Saved in ${savedIn}`
344
- }
345
- </span>
346
- </div>
347
- <nav class="flex shrink-0 items-center gap-1.5" aria-label="Pages">
348
- {
349
- pageNumber > 1 ? (
350
- <a
351
- href={pageUrl(pageNumber - 1)}
352
- title="Previous page"
353
- class="p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:text-[rgb(var(--ec-page-text))]"
482
+ {/* Deleting a canvas, after asking (it's deleted for everyone, for good) */}
483
+ <dialog
484
+ id="delete-canvas-dialog"
485
+ role="alertdialog"
486
+ aria-labelledby="delete-canvas-title"
487
+ data-api={canvasesApiUrl}
488
+ class="m-auto w-full max-w-md rounded-xl border p-0 shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] backdrop:bg-black/40"
489
+ >
490
+ <div class="space-y-2 px-5 py-4">
491
+ <h2 id="delete-canvas-title" class="text-base font-semibold">
492
+ Delete this canvas?
493
+ </h2>
494
+ <p class="text-sm text-[rgb(var(--ec-page-text-muted))]">
495
+ It's deleted for everyone, with its comments, and can't be brought back. Anyone who has it open is told.
496
+ </p>
497
+ <p data-error hidden class:list={['text-sm', STATUS.danger.text]} />
498
+ </div>
499
+ <div class="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
500
+ <button
501
+ type="button"
502
+ data-close-dialog
503
+ class="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)]"
354
504
  >
355
- <ChevronLeft className="h-4 w-4" />
356
- </a>
357
- ) : (
358
- <span aria-disabled="true" class="p-1.5 text-[rgb(var(--ec-icon-color))] opacity-30">
359
- <ChevronLeft className="h-4 w-4" />
360
- </span>
361
- )
362
- }
363
- <span class="min-w-[60px] text-center text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))]">
364
- <span class="font-medium text-[rgb(var(--ec-page-text))]">{pageNumber}</span> / {pageCount}
365
- </span>
366
- {
367
- pageNumber < pageCount ? (
368
- <a
369
- href={pageUrl(pageNumber + 1)}
370
- title="Next page"
371
- class="p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:text-[rgb(var(--ec-page-text))]"
505
+ Cancel
506
+ </button>
507
+ <button
508
+ type="button"
509
+ data-confirm-delete
510
+ class:list={['rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-60', STATUS.danger.button]}
372
511
  >
373
- <ChevronRight className="h-4 w-4" />
374
- </a>
375
- ) : (
376
- <span aria-disabled="true" class="p-1.5 text-[rgb(var(--ec-icon-color))] opacity-30">
377
- <ChevronRight className="h-4 w-4" />
378
- </span>
379
- )
380
- }
381
- </nav>
382
- </div>
383
- </main>
512
+ Delete canvas
513
+ </button>
514
+ </div>
515
+ </dialog>
384
516
 
385
- {/* Naming a new canvas before it opens (New canvas links work without it: they start an untitled one) */}
386
- <dialog
387
- id="new-canvas-dialog"
388
- aria-labelledby="new-canvas-title"
389
- class="m-auto w-full max-w-md rounded-xl border p-0 shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] backdrop:bg-black/40"
390
- >
391
- <form method="post" action={buildUrl('/studio/new')}>
392
- <div class="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
393
- <h2 id="new-canvas-title" class="text-base font-semibold">New canvas</h2>
394
- <p class="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
395
- Name it for what you're designing, so your team can find it.
396
- </p>
397
- </div>
398
- <div class="px-5 py-4">
399
- <label for="new-canvas-name" class="block text-sm font-semibold">Name</label>
400
- <input
401
- id="new-canvas-name"
402
- name="title"
403
- maxlength="200"
404
- autocomplete="off"
405
- placeholder="e.g. Payments redesign"
406
- class="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)]"
407
- />
408
- {/* Who's starting it: the name they use on canvases, if they've joined one in this browser */}
409
- <input type="hidden" name="by" />
410
- </div>
411
- <div class="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
412
- <button
413
- type="button"
414
- data-close-dialog
415
- class="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)]"
517
+ {/* Your name: asked for on your first visit, changed from the button by New canvas */}
518
+ <dialog
519
+ id="name-dialog"
520
+ aria-labelledby="name-dialog-title"
521
+ class="m-auto w-full max-w-md rounded-xl border p-0 shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] backdrop:bg-black/40"
416
522
  >
417
- Cancel
418
- </button>
419
- <button
420
- type="submit"
421
- class="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))]"
422
- >
423
- Create canvas
424
- </button>
425
- </div>
426
- </form>
427
- </dialog>
428
-
429
- {/* Your name: asked for on your first visit, changed from the button by New canvas */}
430
- <dialog
431
- id="name-dialog"
432
- aria-labelledby="name-dialog-title"
433
- class="m-auto w-full max-w-md rounded-xl border p-0 shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))] backdrop:bg-black/40"
434
- >
435
- <form>
436
- <div class="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
437
- <h2 id="name-dialog-title" class="text-base font-semibold" data-first-visit-text="Welcome to Studio">Your name</h2>
438
- <p class="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
439
- What should others call you? It's shown by your pointer and your comments on every canvas.
440
- </p>
441
- </div>
442
- <div class="px-5 py-4">
443
- <label for="your-name" class="block text-sm font-semibold">Name</label>
444
- <input
445
- id="your-name"
446
- name="name"
447
- required
448
- maxlength="60"
449
- autocomplete="name"
450
- placeholder="e.g. Sam"
451
- class="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)]"
452
- />
453
- <p class="mt-2 text-xs text-[rgb(var(--ec-page-text-muted))]">Kept in this browser. Change it here any time.</p>
454
- </div>
455
- <div class="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
456
- <button
457
- type="button"
458
- data-close-dialog
459
- class="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)]"
460
- >
461
- Cancel
462
- </button>
463
- <button
464
- type="submit"
465
- class="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))]"
466
- >
467
- Save
468
- </button>
469
- </div>
470
- </form>
471
- </dialog>
523
+ <form>
524
+ <div class="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
525
+ <h2 id="name-dialog-title" class="text-base font-semibold" data-first-visit-text="Welcome to Studio">
526
+ Your name
527
+ </h2>
528
+ <p class="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
529
+ What should others call you? It's shown by your pointer and your comments on every canvas.
530
+ </p>
531
+ </div>
532
+ <div class="px-5 py-4">
533
+ <label for="your-name" class="block text-sm font-semibold">
534
+ Name
535
+ </label>
536
+ <input
537
+ id="your-name"
538
+ name="name"
539
+ required
540
+ maxlength={MAX_NAME_LENGTH}
541
+ autocomplete="name"
542
+ placeholder="e.g. Sam"
543
+ class="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)]"
544
+ />
545
+ <p class="mt-2 text-xs text-[rgb(var(--ec-page-text-muted))]">Kept in this browser. Change it here any time.</p>
546
+ </div>
547
+ <div class="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
548
+ <button
549
+ type="button"
550
+ data-close-dialog
551
+ class="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)]"
552
+ >
553
+ Cancel
554
+ </button>
555
+ <button
556
+ type="submit"
557
+ class="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))]"
558
+ >
559
+ Save
560
+ </button>
561
+ </div>
562
+ </form>
563
+ </dialog>
564
+ </Fragment>
565
+ )
566
+ }
472
567
  </VerticalSideBarLayout>
473
568
 
474
569
  <script>
475
570
  import { colorForName, getStoredName, onStoredNameChange, storeName } from '../identity';
571
+ import { copyCanvasThroughApi, createCanvasThroughApi, deleteCanvasThroughApi } from '../api/client';
476
572
 
477
573
  // On every visit to the page, including ones the app's client-side navigation swaps in (its scripts run only once)
478
574
  const setUp = () => {
@@ -533,9 +629,136 @@ const strokeOf = (preview: NonNullable<CanvasSummary['preview']>, ratio: number)
533
629
  const open = () => {
534
630
  const by = dialog.querySelector<HTMLInputElement>('input[name="by"]');
535
631
  if (by) by.value = yourName ?? '';
632
+ dialog.querySelector<HTMLElement>('[data-error]')!.hidden = true;
536
633
  dialog.showModal();
537
634
  dialog.querySelector<HTMLInputElement>('#new-canvas-name')?.focus();
538
635
  };
636
+ // Created through the Studio API, then opened
637
+ const newCanvasForm = dialog.querySelector<HTMLFormElement>('form')!;
638
+ newCanvasForm.addEventListener('submit', async (event) => {
639
+ event.preventDefault();
640
+ const form = new FormData(newCanvasForm);
641
+ const submit = newCanvasForm.querySelector<HTMLButtonElement>('[type="submit"]')!;
642
+ const error = newCanvasForm.querySelector<HTMLElement>('[data-error]')!;
643
+ submit.disabled = true;
644
+ error.hidden = true;
645
+ try {
646
+ const canvas = await createCanvasThroughApi(newCanvasForm.dataset.api!, {
647
+ title: String(form.get('title') ?? '').trim() || undefined,
648
+ createdBy: String(form.get('by') ?? '').trim() || undefined,
649
+ });
650
+ window.location.assign(canvas.url);
651
+ } catch (failure) {
652
+ error.textContent = (failure as Error).message;
653
+ error.hidden = false;
654
+ submit.disabled = false;
655
+ }
656
+ });
657
+
658
+ // A canvas's menu (from the ⋮ on its row): make a copy, or delete it
659
+ const rowMenu = document.querySelector<HTMLElement>('#canvas-row-menu')!;
660
+ const copyItem = rowMenu.querySelector<HTMLButtonElement>('[data-menu-copy]')!;
661
+ const copyLabel = copyItem.querySelector<HTMLElement>('[data-label]')!;
662
+ const menuError = rowMenu.querySelector<HTMLElement>('[data-error]')!;
663
+ let menuFor: HTMLButtonElement | undefined;
664
+ const closeMenu = () => {
665
+ rowMenu.hidden = true;
666
+ menuFor?.setAttribute('aria-expanded', 'false');
667
+ menuFor = undefined;
668
+ };
669
+ const openMenu = (button: HTMLButtonElement, fromKeyboard: boolean) => {
670
+ closeMenu();
671
+ menuFor = button;
672
+ button.setAttribute('aria-expanded', 'true');
673
+ copyItem.disabled = false;
674
+ copyLabel.textContent = 'Make a copy';
675
+ menuError.hidden = true;
676
+ rowMenu.hidden = false;
677
+ // Under the button, lined up with its right edge (above it if there's no room below)
678
+ const at = button.getBoundingClientRect();
679
+ const { width, height } = rowMenu.getBoundingClientRect();
680
+ const below = at.bottom + 4 + height <= window.innerHeight;
681
+ rowMenu.style.top = `${below ? at.bottom + 4 : at.top - 4 - height}px`;
682
+ rowMenu.style.left = `${Math.max(8, at.right - width)}px`;
683
+ // Opened with the keyboard: its first item, ready to choose
684
+ if (fromKeyboard) copyItem.focus();
685
+ };
686
+ document.querySelectorAll<HTMLButtonElement>('[data-canvas-menu]').forEach((button) =>
687
+ // A click with no pointer (detail 0) is the keyboard (Enter or Space)
688
+ button.addEventListener('click', (event) => (menuFor === button ? closeMenu() : openMenu(button, event.detail === 0)))
689
+ );
690
+ const onPointerDown = (event: PointerEvent) => {
691
+ const target = event.target as Node;
692
+ if (!rowMenu.hidden && !rowMenu.contains(target) && !menuFor?.contains(target)) closeMenu();
693
+ };
694
+ const onKeyDown = (event: KeyboardEvent) => {
695
+ if (event.key !== 'Escape' || rowMenu.hidden) return;
696
+ const button = menuFor;
697
+ closeMenu();
698
+ button?.focus();
699
+ };
700
+ window.addEventListener('pointerdown', onPointerDown);
701
+ window.addEventListener('keydown', onKeyDown);
702
+ // It's placed for where the row was
703
+ window.addEventListener('resize', closeMenu);
704
+ document.querySelector('[data-canvas-list]')?.addEventListener('scroll', closeMenu, { passive: true });
705
+ document.addEventListener(
706
+ 'astro:before-swap',
707
+ () => {
708
+ window.removeEventListener('pointerdown', onPointerDown);
709
+ window.removeEventListener('keydown', onKeyDown);
710
+ window.removeEventListener('resize', closeMenu);
711
+ },
712
+ { once: true }
713
+ );
714
+
715
+ // Making a copy: it's listed at the top (the canvas changed most recently)
716
+ copyItem.addEventListener('click', async () => {
717
+ const canvasId = menuFor?.dataset.canvasMenu;
718
+ if (!canvasId) return;
719
+ copyItem.disabled = true;
720
+ copyLabel.textContent = 'Copying…';
721
+ menuError.hidden = true;
722
+ try {
723
+ await copyCanvasThroughApi(rowMenu.dataset.api!, canvasId, { createdBy: yourName ?? undefined });
724
+ window.location.reload();
725
+ } catch (failure) {
726
+ menuError.textContent = (failure as Error).message;
727
+ menuError.hidden = false;
728
+ copyItem.disabled = false;
729
+ copyLabel.textContent = 'Make a copy';
730
+ }
731
+ });
732
+
733
+ // Deleting a canvas from the list, after asking
734
+ const deleteDialog = document.querySelector<HTMLDialogElement>('#delete-canvas-dialog')!;
735
+ const deleteTitle = deleteDialog.querySelector<HTMLElement>('#delete-canvas-title')!;
736
+ const deleteError = deleteDialog.querySelector<HTMLElement>('[data-error]')!;
737
+ const confirmDelete = deleteDialog.querySelector<HTMLButtonElement>('[data-confirm-delete]')!;
738
+ let deleting: string | undefined;
739
+ rowMenu.querySelector('[data-menu-delete]')!.addEventListener('click', () => {
740
+ deleting = menuFor?.dataset.canvasMenu;
741
+ const title = menuFor?.dataset.canvasTitle;
742
+ closeMenu();
743
+ deleteTitle.textContent = `Delete ${title ? `"${title}"` : 'this canvas'}?`;
744
+ deleteError.hidden = true;
745
+ confirmDelete.disabled = false;
746
+ deleteDialog.showModal();
747
+ });
748
+ deleteDialog.querySelector('[data-close-dialog]')!.addEventListener('click', () => deleteDialog.close());
749
+ confirmDelete.addEventListener('click', async () => {
750
+ if (!deleting) return;
751
+ confirmDelete.disabled = true;
752
+ deleteError.hidden = true;
753
+ try {
754
+ await deleteCanvasThroughApi(deleteDialog.dataset.api!, deleting);
755
+ window.location.reload();
756
+ } catch (failure) {
757
+ deleteError.textContent = (failure as Error).message;
758
+ deleteError.hidden = false;
759
+ confirmDelete.disabled = false;
760
+ }
761
+ });
539
762
  // Starting a canvas says who started it: ask for a name first if there isn't one yet
540
763
  const startCanvas = () => (yourName ? open() : askName(open));
541
764
  // A plain button, so Enter in the name creates the canvas
@@ -546,7 +769,7 @@ const strokeOf = (preview: NonNullable<CanvasSummary['preview']>, ratio: number)
546
769
  startCanvas();
547
770
  })
548
771
  );
549
- // Opened from a canvas's New canvas (/studio?new=1)
772
+ // Opened from a link (/studio?new=1), e.g. a canvas's New canvas
550
773
  const url = new URL(window.location.href);
551
774
  if (url.searchParams.has('new')) {
552
775
  url.searchParams.delete('new');