@eventcatalog/core 5.0.0-beta.2 → 5.0.0-beta.20

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 (204) 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 +71 -35
  8. package/dist/analytics/log-build.js +5 -4
  9. package/dist/analytics/telemetry.cjs +42 -0
  10. package/dist/analytics/telemetry.d.cts +20 -0
  11. package/dist/analytics/telemetry.d.ts +20 -0
  12. package/dist/analytics/telemetry.js +8 -0
  13. package/dist/catalog-runtime.cjs +1 -1
  14. package/dist/{chunk-HGRFXTFC.js → chunk-3VQSVQTU.js} +1 -1
  15. package/dist/{chunk-KIDISU6X.js → chunk-5ZUN2TXB.js} +56 -33
  16. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  17. package/dist/chunk-CPW6G62V.js +17 -0
  18. package/dist/{chunk-4J2ZM2HX.js → chunk-F236OJZD.js} +1 -1
  19. package/dist/{chunk-XARPIXWF.js → chunk-FWH5T6U4.js} +9 -3
  20. package/dist/{chunk-62EV4IJA.js → chunk-R2X4ZEF6.js} +1 -1
  21. package/dist/constants.cjs +1 -1
  22. package/dist/constants.js +1 -1
  23. package/dist/eventcatalog.cjs +214 -129
  24. package/dist/eventcatalog.config.d.cts +59 -1
  25. package/dist/eventcatalog.config.d.ts +59 -1
  26. package/dist/eventcatalog.js +39 -15
  27. package/dist/federation/federate.js +2 -2
  28. package/dist/generate.cjs +1 -1
  29. package/dist/generate.js +3 -3
  30. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  31. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  32. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  33. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  34. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  35. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  36. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  37. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  38. package/dist/runtime-dependencies/manifest.json +13 -1
  39. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  40. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  41. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  42. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  43. package/dist/runtime-dependencies/yjs.mjs +3 -0
  44. package/dist/utils/cli-logger.cjs +1 -1
  45. package/dist/utils/cli-logger.js +2 -2
  46. package/dist/utils/license-status.cjs +71 -45
  47. package/dist/utils/license-status.d.cts +14 -5
  48. package/dist/utils/license-status.d.ts +14 -5
  49. package/dist/utils/license-status.js +1 -1
  50. package/eventcatalog/astro.config.mjs +23 -1
  51. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  52. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  53. package/eventcatalog/integrations/storage.mjs +20 -0
  54. package/eventcatalog/integrations/studio-server.mjs +32 -0
  55. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  56. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  57. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  58. package/eventcatalog/src/components/Header.astro +3 -3
  59. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  60. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
  61. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  62. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  63. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  64. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  65. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  66. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  67. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  68. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  69. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  70. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  71. package/eventcatalog/src/components/Seo.astro +5 -1
  72. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  73. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  74. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  75. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  76. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  77. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  78. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  79. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  80. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  81. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  82. package/eventcatalog/src/features/auth/error.astro +4 -2
  83. package/eventcatalog/src/features/auth/login.astro +2 -1
  84. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  85. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  86. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  87. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  88. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  89. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  90. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  91. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  92. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  93. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +77 -76
  94. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  95. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  96. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  97. package/eventcatalog/src/features/server/start.ts +25 -0
  98. package/eventcatalog/src/features/storage/database.ts +88 -0
  99. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  100. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  101. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  102. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  103. package/eventcatalog/src/features/storage/sql.ts +28 -0
  104. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  105. package/eventcatalog/src/features/studio/agents.ts +28 -0
  106. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  107. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  108. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  109. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  110. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  111. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  112. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  113. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  114. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  115. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  116. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  117. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  118. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  119. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  120. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +203 -0
  121. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  122. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  123. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  124. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  125. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  126. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  127. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  128. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  129. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  130. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  131. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  132. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  133. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  134. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  135. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  136. package/eventcatalog/src/features/studio/identity.ts +59 -0
  137. package/eventcatalog/src/features/studio/limits.ts +2 -0
  138. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  139. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  140. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  141. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  142. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  143. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  144. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  145. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  146. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  147. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  148. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  149. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -7
  150. package/eventcatalog/src/pages/_index.astro +1 -1
  151. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  152. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  153. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  154. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  155. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  156. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  157. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  158. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +1 -1
  159. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  160. package/eventcatalog/src/pages/index.astro +1 -1
  161. package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
  162. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +1 -1
  163. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  164. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  165. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  166. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  167. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  168. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  169. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  170. package/eventcatalog/src/styles/tailwind.css +2 -2
  171. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  172. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  173. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  174. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  175. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  176. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  177. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  178. package/eventcatalog/src/utils/collections/util.ts +31 -0
  179. package/eventcatalog/src/utils/feature.ts +8 -0
  180. package/eventcatalog/src/utils/git.ts +11 -5
  181. package/eventcatalog/src/utils/link-validation.ts +25 -19
  182. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  183. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  184. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  185. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  186. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  187. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  188. package/eventcatalog/src/utils/page-title.ts +15 -0
  189. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  190. package/eventcatalog/src/utils/trial.ts +1 -1
  191. package/package.json +22 -16
  192. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  193. package/eventcatalog/public/studio-bg.png +0 -0
  194. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  195. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  196. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  197. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  198. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  199. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  200. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  203. package/eventcatalog/src/pages/studio.astro +0 -316
  204. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -1,7 +1,130 @@
1
1
  // Shared utilities for NestedSideBar components
2
2
 
3
+ import type { ChildRef, NavigationData, NavNode } from '@stores/sidebar-store/state';
4
+
3
5
  export const SIDEBAR_GROUP_COLLAPSE_THRESHOLD = 5;
4
6
 
7
+ /**
8
+ * Strip the configured base path from a pathname so URL pattern matching works
9
+ * regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
10
+ */
11
+ export const stripBasePath = (pathname: string): string => {
12
+ const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
13
+ if (base && pathname.startsWith(base)) {
14
+ return pathname.slice(base.length) || '/';
15
+ }
16
+ return pathname;
17
+ };
18
+
19
+ export type NavigationLevel = {
20
+ key: string | null; // The key of the node that was drilled into (null for root)
21
+ entries: ChildRef[];
22
+ title: string;
23
+ badge?: string; // Category badge (e.g., "Domain", "Service")
24
+ };
25
+
26
+ export const rootLevel = (roots: ChildRef[]): NavigationLevel => ({ key: null, entries: roots, title: 'Documentation' });
27
+
28
+ /** Resolve a child reference to a node, following unversioned aliases (string → key). */
29
+ export const resolveNavRef = (nodes: NavigationData['nodes'], ref: ChildRef): NavNode | null => {
30
+ if (typeof ref !== 'string') return ref;
31
+ const node = nodes[ref];
32
+ if (!node) return null;
33
+ if (typeof node === 'string') return (nodes[node] as NavNode) ?? null;
34
+ return node;
35
+ };
36
+
37
+ /** Map of "type:id" → versioned node key, used to match URLs to nodes. */
38
+ export const buildNodeLookup = (nodes: NavigationData['nodes']): Map<string, string> => {
39
+ const lookup = new Map<string, string>();
40
+
41
+ Object.entries(nodes).forEach(([key, value]) => {
42
+ // Skip keys that are references (string values pointing to other keys)
43
+ // These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
44
+ if (typeof value === 'string') return;
45
+
46
+ // Key formats:
47
+ // - "type:id:version" (e.g., "service:OrdersService:0.0.3")
48
+ // - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
49
+ // - "list:name" (e.g., "list:domains") - skip these
50
+ const parts = key.split(':');
51
+
52
+ // Skip list items
53
+ if (parts[0] === 'list') return;
54
+
55
+ if (parts.length >= 2) {
56
+ // Store as "type:id"
57
+ lookup.set(`${parts[0]}:${parts[1]}`, key);
58
+ }
59
+ });
60
+
61
+ return lookup;
62
+ };
63
+
64
+ /**
65
+ * What the sidebar shows for a URL before it knows the reader's saved drill-down path:
66
+ * the root level on the homepage or for pages without children, otherwise the matching
67
+ * resource's own level (the same "flattened" stack the client falls back to).
68
+ */
69
+ export const getStackForUrl = (
70
+ url: string,
71
+ data: NavigationData,
72
+ nodeLookup: ReadonlyMap<string, string> = buildNodeLookup(data.nodes)
73
+ ): NavigationLevel[] => {
74
+ const root = rootLevel(data.roots);
75
+ if (url === '/' || url === '') return [root];
76
+
77
+ const targetKey = findNodeKeyByUrl(url, data.nodes, nodeLookup);
78
+ const target = targetKey ? resolveNavRef(data.nodes, targetKey) : null;
79
+ if (!targetKey || !target?.pages?.length) return [root];
80
+
81
+ return [root, { key: targetKey, entries: target.pages, title: target.title, badge: target.badge }];
82
+ };
83
+
84
+ /**
85
+ * Everything the sidebar needs to render one page's level, without the whole catalog: the
86
+ * stack for the URL and only the nodes that level shows (its items, and the contents of its
87
+ * groups). Rendered on the server so the sidebar appears with the HTML, and handed to the
88
+ * island so it keeps showing it until the full navigation data loads.
89
+ */
90
+ export type SidebarSnapshot = {
91
+ url: string;
92
+ stack: NavigationLevel[];
93
+ data: NavigationData;
94
+ };
95
+
96
+ const nodeLookupCache = new WeakMap<NavigationData, Map<string, string>>();
97
+
98
+ export const buildSidebarSnapshot = (data: NavigationData, url: string): SidebarSnapshot => {
99
+ let nodeLookup = nodeLookupCache.get(data);
100
+ if (!nodeLookup) {
101
+ nodeLookup = buildNodeLookup(data.nodes);
102
+ nodeLookupCache.set(data, nodeLookup);
103
+ }
104
+
105
+ const stack = getStackForUrl(url, data, nodeLookup);
106
+ const level = stack[stack.length - 1];
107
+ const nodes: NavigationData['nodes'] = {};
108
+
109
+ const include = (ref: ChildRef) => {
110
+ if (typeof ref === 'string') {
111
+ if (ref in nodes) return;
112
+ const value = data.nodes[ref];
113
+ if (value === undefined) return;
114
+ nodes[ref] = value;
115
+ if (typeof value === 'string' && data.nodes[value] !== undefined) nodes[value] = data.nodes[value];
116
+ }
117
+ const node = resolveNavRef(data.nodes, ref);
118
+ // Groups render their children inline; items with children only need their own node
119
+ if (node?.type === 'group') node.pages?.forEach(include);
120
+ };
121
+
122
+ if (level.key) include(level.key);
123
+ level.entries.forEach(include);
124
+
125
+ return { url, stack, data: { roots: data.roots, nodes } };
126
+ };
127
+
5
128
  /**
6
129
  * The user's overrides of each group's default collapse state, keyed by group id.
7
130
  * A group is in at most one set; absence means "use the group's default".
@@ -67,6 +190,14 @@ export const toggleGroupCollapsed = (
67
190
  * Finds the sidebar node represented by a resource URL.
68
191
  * The URL must have its configured base path removed before it is passed in.
69
192
  */
193
+ /**
194
+ * Whether an item in a group drops its default per-collection icon because the group heading
195
+ * already conveys its type: every item under a nested group, and items whose icon is the
196
+ * group's own (e.g. events listed under Events). Custom icons (`leftIcon`) always show.
197
+ */
198
+ export const hidesDefaultItemIcon = (group: NavNode, item: NavNode, isNested: boolean): boolean =>
199
+ isNested || (!!group.icon && group.icon === item.icon);
200
+
70
201
  export const findNodeKeyByUrl = (
71
202
  url: string,
72
203
  nodes: Record<string, unknown>,
@@ -3,10 +3,18 @@ import type { HTMLAttributes } from 'astro/types';
3
3
 
4
4
  interface Props extends Omit<HTMLAttributes<'div'>, 'children'> {}
5
5
  import NestedSideBar from './NestedSideBar';
6
+ import { buildSidebarSnapshot, stripBasePath } from './NestedSideBar/utils';
7
+ import { getNestedSideBarData } from '@stores/sidebar-store/state';
6
8
  import { ClientRouter } from 'astro:transitions';
9
+
10
+ // Render this page's sidebar level into the HTML so it shows before any JavaScript runs;
11
+ // the full navigation still loads from /api/sidebar-data.json (see SearchDataLoader)
12
+ const snapshot = buildSidebarSnapshot(await getNestedSideBarData(), stripBasePath(Astro.url.pathname));
7
13
  ---
8
14
 
9
15
  <div {...Astro.props}>
10
16
  <ClientRouter />
11
- <NestedSideBar client:only="react" />
17
+ <NestedSideBar client:only="react" snapshot={snapshot}>
18
+ <NestedSideBar slot="fallback" snapshot={snapshot} />
19
+ </NestedSideBar>
12
20
  </div>
@@ -11,7 +11,7 @@ import {
11
11
  type ColumnFiltersState,
12
12
  type SortingState,
13
13
  } from '@tanstack/react-table';
14
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, SearchX, X, Search, Users } from 'lucide-react';
14
+ import { ArrowDown, ArrowUp, ArrowUpDown, SearchX, X, Search, Users } from 'lucide-react';
15
15
  import { UserIcon } from '@heroicons/react/24/outline';
16
16
  import { useEffect, useMemo, useState } from 'react';
17
17
  import type { TableConfiguration } from '@types';
@@ -19,6 +19,7 @@ import { isSameVersion } from '@utils/collections/version-compare';
19
19
  import { resolveIconUrl } from '@utils/icon';
20
20
  import { FilterDropdown, CheckboxItem } from './FilterComponents';
21
21
  import { getDiscoverColumns } from './columns';
22
+ import { TablePagination, useStoredPageSize } from '../TablePagination';
22
23
  import { formatAdrStatus, type AdrStatus } from '@utils/collections/adr-constants';
23
24
  import { buildDiscoverFilterSearch, filterKnownValues, parseDiscoverFilterSearch } from './url-filters';
24
25
 
@@ -243,18 +244,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
243
244
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
244
245
  const [sorting, setSorting] = useState<SortingState>([]);
245
246
  const [tableFilter, setTableFilter] = useState(initialUrlFilters.q);
246
- const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
247
- const PAGE_SIZE_STORAGE_KEY = 'eventcatalog-discover-page-size';
248
- const [pageSize, setPageSize] = useState<number>(() => {
249
- if (typeof window === 'undefined') return 10;
250
- const stored = Number(window.localStorage.getItem(PAGE_SIZE_STORAGE_KEY));
251
- return PAGE_SIZE_OPTIONS.includes(stored) ? stored : 10;
252
- });
253
- useEffect(() => {
254
- if (typeof window !== 'undefined') {
255
- window.localStorage.setItem(PAGE_SIZE_STORAGE_KEY, String(pageSize));
256
- }
257
- }, [pageSize]);
247
+ const [pageSize, setPageSize] = useStoredPageSize('eventcatalog-discover-page-size');
258
248
  const [showOnlyLatest, setShowOnlyLatest] = useState(initialUrlFilters.showOnlyLatest);
259
249
  const [onlyShowDrafts, setOnlyShowDrafts] = useState(initialUrlFilters.onlyShowDrafts);
260
250
  const [selectedDomains, setSelectedDomains] = useState<string[]>(() =>
@@ -1287,55 +1277,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
1287
1277
  </div>
1288
1278
  </div>
1289
1279
 
1290
- {/* Pagination */}
1291
- <div className="flex-shrink-0 flex items-center justify-between px-6 py-3 border-t border-[rgb(var(--ec-page-border))]">
1292
- <div className="flex items-center gap-3 text-xs text-[rgb(var(--ec-page-text-muted))]">
1293
- {totalResults > 0 && (
1294
- <span>
1295
- <span className="font-medium text-[rgb(var(--ec-page-text))]">{table.getRowModel().rows.length}</span> of{' '}
1296
- <span className="font-medium text-[rgb(var(--ec-page-text))]">{totalResults}</span> results
1297
- </span>
1298
- )}
1299
- {totalResults > 0 && <span aria-hidden className="h-3 w-px bg-[rgb(var(--ec-page-border))]" />}
1300
- <label className="flex items-center gap-1.5">
1301
- <span>Per page</span>
1302
- <select
1303
- value={pageSize}
1304
- onChange={(e) => setPageSize(Number(e.target.value))}
1305
- className="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"
1306
- >
1307
- {PAGE_SIZE_OPTIONS.map((size) => (
1308
- <option key={size} value={size}>
1309
- {size}
1310
- </option>
1311
- ))}
1312
- </select>
1313
- </label>
1314
- </div>
1315
- <div className="flex items-center gap-1.5">
1316
- <button
1317
- className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
1318
- onClick={() => table.previousPage()}
1319
- disabled={!table.getCanPreviousPage()}
1320
- title="Previous page"
1321
- >
1322
- <ChevronLeft className="w-4 h-4" />
1323
- </button>
1324
- <span className="text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))] min-w-[60px] text-center">
1325
- <span className="font-medium text-[rgb(var(--ec-page-text))]">{table.getState().pagination.pageIndex + 1}</span>
1326
- {' / '}
1327
- <span>{table.getPageCount() || 1}</span>
1328
- </span>
1329
- <button
1330
- className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
1331
- onClick={() => table.nextPage()}
1332
- disabled={!table.getCanNextPage()}
1333
- title="Next page"
1334
- >
1335
- <ChevronRight className="w-4 h-4" />
1336
- </button>
1337
- </div>
1338
- </div>
1280
+ <TablePagination table={table} pageSize={pageSize} onPageSizeChange={setPageSize} />
1339
1281
  </div>
1340
1282
  </div>
1341
1283
  );
@@ -1,25 +1,16 @@
1
1
  import { createColumnHelper } from '@tanstack/react-table';
2
- import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { DocumentTextIcon, MapIcon } from '@heroicons/react/24/solid';
5
- import {
6
- ArrowDownIcon,
7
- ArrowTopRightOnSquareIcon,
8
- ArrowUpIcon,
9
- EllipsisVerticalIcon,
10
- StarIcon,
11
- } from '@heroicons/react/24/outline';
2
+ import { useMemo, useState } from 'react';
3
+ import { ArrowDownIcon, ArrowTopRightOnSquareIcon, ArrowUpIcon } from '@heroicons/react/24/outline';
12
4
  import { buildUrl } from '@utils/url-builder';
13
5
  import { getColorAndIconForCollection } from '@utils/collections/icons';
14
6
  import { getCollectionTextColorClass } from '@utils/collection-colors';
15
7
  import { getBadgeHref, getBadgeReactStyle } from '@utils/badge-styles';
16
8
  import { isIconPath, resolveIconUrl } from '@utils/icon';
17
- import { useStore } from '@nanostores/react';
18
- import { favoritesStore, toggleFavorite, type FavoriteItem } from '../../../stores/favorites-store';
19
9
  import type { DiscoverTableData, CollectionType } from './DiscoverTable';
20
10
  import type { TableConfiguration } from '@types';
21
11
  import { formatAdrDate, isAdrCollection } from '@utils/collections/adr-constants';
22
12
  import { CornerDownRight } from 'lucide-react';
13
+ import { RowActionsMenu } from '../RowActionsMenu';
23
14
 
24
15
  const columnHelper = createColumnHelper<DiscoverTableData>();
25
16
 
@@ -167,147 +158,23 @@ const ResourceNameCell = ({ item }: { item: DiscoverTableData }) => {
167
158
  );
168
159
  };
169
160
 
170
- const MENU_WIDTH = 280;
171
- const MENU_GAP = 6;
172
-
173
- const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; collectionType: CollectionType }) => {
174
- const [isOpen, setIsOpen] = useState(false);
175
- const buttonRef = useRef<HTMLButtonElement>(null);
176
- const menuRef = useRef<HTMLDivElement>(null);
177
- const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
178
- const favorites = useStore(favoritesStore);
179
- const href = buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`);
180
- const visualiserHref = buildUrl(`/visualiser/${item.collection}/${item.data.id}/${item.data.version}`);
181
- const hasVisualiser = !isAdrCollection(item.collection);
182
- const nodeKey = `${item.collection}-${item.data.id}-${item.data.version}`;
183
- const badgeLabel =
161
+ // Discover keys favorites as `${collection}-${id}-${version}`; keep that so existing favorites still match.
162
+ const DiscoverRowActions = ({ item, collectionType }: { item: DiscoverTableData; collectionType: CollectionType }) => {
163
+ const badge =
184
164
  collectionType === 'external-systems'
185
165
  ? 'External System'
186
166
  : collectionType.charAt(0).toUpperCase() + collectionType.slice(1, -1);
187
- const isFavorite = favorites.some((fav) => fav.nodeKey === nodeKey);
188
-
189
- // The table lives inside scrollable, overflow-hidden containers, so the menu is
190
- // rendered in a portal with fixed positioning to avoid being clipped.
191
- const updatePosition = () => {
192
- const button = buttonRef.current;
193
- if (!button) return;
194
- const rect = button.getBoundingClientRect();
195
- const menuHeight = menuRef.current?.offsetHeight ?? 0;
196
- const spaceBelow = window.innerHeight - rect.bottom;
197
-
198
- // Flip above the trigger when there isn't enough room below it.
199
- const openUpwards = menuHeight > 0 && spaceBelow < menuHeight + MENU_GAP && rect.top > spaceBelow;
200
- const top = openUpwards ? rect.top - menuHeight - MENU_GAP : rect.bottom + MENU_GAP;
201
- const left = Math.max(MENU_GAP, rect.right - MENU_WIDTH);
202
-
203
- setMenuPosition({ top, left });
204
- };
205
-
206
- useLayoutEffect(() => {
207
- if (!isOpen) return;
208
- updatePosition();
209
- // eslint-disable-next-line react-hooks/exhaustive-deps
210
- }, [isOpen]);
211
-
212
- useEffect(() => {
213
- if (!isOpen) return;
214
-
215
- const handlePointerDown = (event: MouseEvent) => {
216
- const target = event.target as Node;
217
- if (!menuRef.current?.contains(target) && !buttonRef.current?.contains(target)) {
218
- setIsOpen(false);
219
- }
220
- };
221
-
222
- const handleEscape = (event: KeyboardEvent) => {
223
- if (event.key === 'Escape') {
224
- setIsOpen(false);
225
- }
226
- };
227
-
228
- const handleReposition = () => updatePosition();
229
-
230
- document.addEventListener('mousedown', handlePointerDown);
231
- document.addEventListener('keydown', handleEscape);
232
- window.addEventListener('resize', handleReposition);
233
- window.addEventListener('scroll', handleReposition, true);
234
-
235
- return () => {
236
- document.removeEventListener('mousedown', handlePointerDown);
237
- document.removeEventListener('keydown', handleEscape);
238
- window.removeEventListener('resize', handleReposition);
239
- window.removeEventListener('scroll', handleReposition, true);
240
- };
241
- }, [isOpen]);
242
-
243
- const handleToggleFavorite = () => {
244
- const favoriteItem: FavoriteItem = {
245
- nodeKey,
246
- path: [],
247
- title: item.data.name,
248
- badge: badgeLabel,
249
- href,
250
- };
251
- toggleFavorite(favoriteItem);
252
- setIsOpen(false);
253
- };
254
-
255
167
  return (
256
- <div className="relative flex justify-end">
257
- <button
258
- ref={buttonRef}
259
- type="button"
260
- aria-haspopup="menu"
261
- aria-expanded={isOpen}
262
- onClick={() => setIsOpen((prev) => !prev)}
263
- className="rounded-md p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:bg-[rgb(var(--ec-content-hover)/0.5)] hover:text-[rgb(var(--ec-page-text))]"
264
- >
265
- <EllipsisVerticalIcon className="h-4 w-4" />
266
- </button>
267
-
268
- {isOpen &&
269
- typeof document !== 'undefined' &&
270
- createPortal(
271
- <div
272
- ref={menuRef}
273
- role="menu"
274
- style={{
275
- position: 'fixed',
276
- top: menuPosition?.top ?? -9999,
277
- left: menuPosition?.left ?? -9999,
278
- width: MENU_WIDTH,
279
- visibility: menuPosition ? 'visible' : 'hidden',
280
- }}
281
- className="z-50 overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] shadow-xl"
282
- >
283
- <a
284
- href={href}
285
- className="flex items-center gap-2.5 px-3 py-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
286
- >
287
- <DocumentTextIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
288
- View documentation
289
- </a>
290
- {hasVisualiser && (
291
- <a
292
- href={visualiserHref}
293
- className="flex items-center gap-2.5 px-3 py-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
294
- >
295
- <MapIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
296
- View in visualiser
297
- </a>
298
- )}
299
- <button
300
- type="button"
301
- onClick={handleToggleFavorite}
302
- className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
303
- >
304
- <StarIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
305
- {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
306
- </button>
307
- </div>,
308
- document.body
309
- )}
310
- </div>
168
+ <RowActionsMenu
169
+ title={item.data.name}
170
+ href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
171
+ visualiserHref={
172
+ isAdrCollection(item.collection)
173
+ ? undefined
174
+ : buildUrl(`/visualiser/${item.collection}/${item.data.id}/${item.data.version}`)
175
+ }
176
+ favorite={{ nodeKey: `${item.collection}-${item.data.id}-${item.data.version}`, badge }}
177
+ />
311
178
  );
312
179
  };
313
180
 
@@ -319,7 +186,7 @@ const createActionsColumn = (collectionType: CollectionType, tableConfiguration:
319
186
  enableSorting: false,
320
187
  cell: (info) => {
321
188
  const item = info.row.original;
322
- return <RowActionsMenu item={item} collectionType={collectionType} />;
189
+ return <DiscoverRowActions item={item} collectionType={collectionType} />;
323
190
  },
324
191
  meta: {
325
192
  showFilter: false,