@eventcatalog/core 4.10.15 → 4.11.1

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 (165) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/astro-output.cjs +60 -1
  6. package/dist/astro-output.js +1 -1
  7. package/dist/{chunk-U52FCZQ6.js → chunk-2YJRF4ZX.js} +1 -1
  8. package/dist/chunk-634NFV5U.js +75 -0
  9. package/dist/{chunk-I6HUOX4R.js → chunk-AXDNSGAN.js} +1 -1
  10. package/dist/chunk-DHH5U5PF.js +53 -0
  11. package/dist/{chunk-A4JRWRCV.js → chunk-DSCIWCX2.js} +1 -1
  12. package/dist/chunk-FWXEV4LF.js +19 -0
  13. package/dist/{chunk-Q2ATWE7N.js → chunk-GZVDXCYF.js} +1 -1
  14. package/dist/{chunk-7LWOCX4R.js → chunk-RYNPEIHQ.js} +1 -1
  15. package/dist/{chunk-HBVTU6ZP.js → chunk-YYVW4DCO.js} +1 -1
  16. package/dist/constants.cjs +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/copy-core.cjs +88 -0
  19. package/dist/copy-core.d.cts +4 -0
  20. package/dist/copy-core.d.ts +4 -0
  21. package/dist/copy-core.js +8 -0
  22. package/dist/eventcatalog.cjs +234 -115
  23. package/dist/eventcatalog.js +36 -30
  24. package/dist/federation/federate.js +3 -3
  25. package/dist/federation/source-provider.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/update-check.cjs +44 -0
  29. package/dist/update-check.d.cts +18 -0
  30. package/dist/update-check.d.ts +18 -0
  31. package/dist/update-check.js +8 -0
  32. package/dist/utils/cli-logger.cjs +1 -1
  33. package/dist/utils/cli-logger.js +2 -2
  34. package/eventcatalog/astro.config.mjs +8 -1
  35. package/eventcatalog/public/icons/languages/android.svg +1 -0
  36. package/eventcatalog/public/icons/languages/angular.svg +1 -0
  37. package/eventcatalog/public/icons/languages/apple.svg +1 -0
  38. package/eventcatalog/public/icons/languages/arduino.svg +1 -0
  39. package/eventcatalog/public/icons/languages/bash.svg +1 -0
  40. package/eventcatalog/public/icons/languages/bun.svg +1 -0
  41. package/eventcatalog/public/icons/languages/c.svg +1 -0
  42. package/eventcatalog/public/icons/languages/clojure.svg +1 -0
  43. package/eventcatalog/public/icons/languages/cpp.svg +1 -0
  44. package/eventcatalog/public/icons/languages/crystal.svg +1 -0
  45. package/eventcatalog/public/icons/languages/csharp.svg +1 -0
  46. package/eventcatalog/public/icons/languages/css.svg +1 -0
  47. package/eventcatalog/public/icons/languages/curl.svg +1 -0
  48. package/eventcatalog/public/icons/languages/dart.svg +1 -0
  49. package/eventcatalog/public/icons/languages/deno.svg +1 -0
  50. package/eventcatalog/public/icons/languages/docker.svg +1 -0
  51. package/eventcatalog/public/icons/languages/elm.svg +1 -0
  52. package/eventcatalog/public/icons/languages/erlang.svg +1 -0
  53. package/eventcatalog/public/icons/languages/flutter.svg +1 -0
  54. package/eventcatalog/public/icons/languages/fortran.svg +1 -0
  55. package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
  56. package/eventcatalog/public/icons/languages/git.svg +1 -0
  57. package/eventcatalog/public/icons/languages/gradle.svg +1 -0
  58. package/eventcatalog/public/icons/languages/graphql.svg +1 -0
  59. package/eventcatalog/public/icons/languages/grpc.svg +1 -0
  60. package/eventcatalog/public/icons/languages/haskell.svg +1 -0
  61. package/eventcatalog/public/icons/languages/hasura.svg +1 -0
  62. package/eventcatalog/public/icons/languages/html.svg +1 -0
  63. package/eventcatalog/public/icons/languages/httpie.svg +1 -0
  64. package/eventcatalog/public/icons/languages/javascript.svg +1 -0
  65. package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
  66. package/eventcatalog/public/icons/languages/json.svg +1 -0
  67. package/eventcatalog/public/icons/languages/julia.svg +1 -0
  68. package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
  69. package/eventcatalog/public/icons/languages/kafka.svg +1 -0
  70. package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
  71. package/eventcatalog/public/icons/languages/lua.svg +1 -0
  72. package/eventcatalog/public/icons/languages/markdown.svg +1 -0
  73. package/eventcatalog/public/icons/languages/maven.svg +1 -0
  74. package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
  75. package/eventcatalog/public/icons/languages/mysql.svg +1 -0
  76. package/eventcatalog/public/icons/languages/nginx.svg +1 -0
  77. package/eventcatalog/public/icons/languages/nim.svg +1 -0
  78. package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
  79. package/eventcatalog/public/icons/languages/perl.svg +1 -0
  80. package/eventcatalog/public/icons/languages/php.svg +1 -0
  81. package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
  82. package/eventcatalog/public/icons/languages/prisma.svg +1 -0
  83. package/eventcatalog/public/icons/languages/purescript.svg +1 -0
  84. package/eventcatalog/public/icons/languages/python.svg +1 -0
  85. package/eventcatalog/public/icons/languages/r.svg +1 -0
  86. package/eventcatalog/public/icons/languages/react.svg +1 -0
  87. package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
  88. package/eventcatalog/public/icons/languages/redis.svg +1 -0
  89. package/eventcatalog/public/icons/languages/ruby.svg +1 -0
  90. package/eventcatalog/public/icons/languages/rust.svg +1 -0
  91. package/eventcatalog/public/icons/languages/sass.svg +1 -0
  92. package/eventcatalog/public/icons/languages/scala.svg +1 -0
  93. package/eventcatalog/public/icons/languages/solidity.svg +1 -0
  94. package/eventcatalog/public/icons/languages/spring.svg +1 -0
  95. package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
  96. package/eventcatalog/public/icons/languages/svelte.svg +1 -0
  97. package/eventcatalog/public/icons/languages/swagger.svg +1 -0
  98. package/eventcatalog/public/icons/languages/swift.svg +1 -0
  99. package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
  100. package/eventcatalog/public/icons/languages/terraform.svg +1 -0
  101. package/eventcatalog/public/icons/languages/toml.svg +1 -0
  102. package/eventcatalog/public/icons/languages/typescript.svg +1 -0
  103. package/eventcatalog/public/icons/languages/unity.svg +1 -0
  104. package/eventcatalog/public/icons/languages/vim.svg +1 -0
  105. package/eventcatalog/public/icons/languages/vue.svg +1 -0
  106. package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
  107. package/eventcatalog/public/icons/languages/yaml.svg +1 -0
  108. package/eventcatalog/public/icons/languages/zig.svg +1 -0
  109. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
  110. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
  111. package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
  112. package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
  113. package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
  114. package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
  115. package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
  116. package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
  117. package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
  118. package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
  119. package/eventcatalog/src/components/MDX/components.tsx +6 -0
  120. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
  121. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
  122. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
  123. package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
  124. package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
  125. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
  126. package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
  127. package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
  128. package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
  129. package/eventcatalog/src/content.config.ts +28 -0
  130. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
  131. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
  132. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
  133. package/eventcatalog/src/plugins/empty-collection-warning.d.ts +21 -0
  134. package/eventcatalog/src/plugins/empty-collection-warning.mjs +107 -0
  135. package/eventcatalog/src/plugins/quiet-empty-collections-logger.mjs +11 -0
  136. package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
  137. package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
  138. package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
  139. package/eventcatalog/src/utils/collections/channels.ts +12 -4
  140. package/eventcatalog/src/utils/collections/examples.ts +133 -41
  141. package/eventcatalog/src/utils/collections/flows.ts +43 -0
  142. package/eventcatalog/src/utils/collections/messages.ts +2 -2
  143. package/eventcatalog/src/utils/collections/schemas.ts +15 -0
  144. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +17 -15
  145. package/eventcatalog/src/utils/schema-explorer.ts +73 -7
  146. package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
  147. package/package.json +5 -4
  148. package/dist/chunk-RDYYZQYU.js +0 -18
  149. package/eventcatalog/src/components/EnvironmentDropdown.test.ts +0 -64
  150. package/eventcatalog/src/components/Grids/message-link.spec.ts +0 -59
  151. package/eventcatalog/src/components/MDX/CustomProperties/custom-properties.spec.ts +0 -38
  152. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +0 -116
  153. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +0 -114
  154. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +0 -91
  155. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +0 -33
  156. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +0 -84
  157. package/eventcatalog/src/components/Search/search-utils.spec.ts +0 -173
  158. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +0 -105
  159. package/eventcatalog/src/components/navigation-progress.spec.ts +0 -68
  160. package/eventcatalog/src/enterprise/directory/user-team-directory.spec.ts +0 -527
  161. package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +0 -302
  162. package/eventcatalog/src/enterprise/fields/fields-db.test.ts +0 -185
  163. package/eventcatalog/src/stores/eventcatalog-store.spec.ts +0 -60
  164. package/eventcatalog/src/utils/collections/glob-loader.spec.ts +0 -87
  165. package/dist/{chunk-6K7XZAYI.js → chunk-A2RZR3U4.js} +3 -3
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useEffect, useRef } from 'react';
1
+ import { Fragment, useState, useMemo, useEffect, useRef } from 'react';
2
2
  import * as Diff from 'diff';
3
3
  import { html } from 'diff2html';
4
4
  import 'diff2html/bundles/css/diff2html.min.css';
@@ -9,10 +9,12 @@ import {
9
9
  ClipboardDocumentIcon,
10
10
  TableCellsIcon,
11
11
  CodeBracketIcon,
12
- ClockIcon,
13
12
  GlobeAltIcon,
14
13
  ServerIcon,
15
- BookOpenIcon,
14
+ CodeBracketSquareIcon,
15
+ RectangleStackIcon,
16
+ ArrowsRightLeftIcon,
17
+ InformationCircleIcon,
16
18
  } from '@heroicons/react/24/outline';
17
19
  import { CheckIcon } from '@heroicons/react/20/solid';
18
20
  import { buildUrl } from '@utils/url-builder';
@@ -21,13 +23,287 @@ import SchemaContentViewer from './SchemaContentViewer';
21
23
  import DiffViewer from './DiffViewer';
22
24
  import ApiContentViewer from './ApiContentViewer';
23
25
  import ExamplesViewer from './ExamplesViewer';
26
+ import SchemaUsage from './SchemaUsage';
27
+ import SchemaFlows from './SchemaFlows';
24
28
  import VersionHistoryModal from './VersionHistoryModal';
25
29
  import SchemaCodeModal from './SchemaCodeModal';
26
30
  import SchemaViewerModal from './SchemaViewerModal';
27
- import { copyToClipboard, downloadSchema, getSchemaTypeLabel, ICON_SPECS, getSchemaRelationshipReference } from './utils';
31
+ import {
32
+ copyToClipboard,
33
+ downloadSchema,
34
+ getSchemaTypeLabel,
35
+ ICON_SPECS,
36
+ getSchemaRelationshipReference,
37
+ getSchemaRelationshipHref,
38
+ SCHEMA_RELATIONSHIP_LABELS,
39
+ } from './utils';
28
40
  import { createSchemaDetailsLoader, useSchemaDetails } from './useSchemaDetails';
29
41
  import { parseProtobufSchema } from '@utils/protobuf-schema';
30
- import type { SchemaItem, VersionDiff, Owner, Producer, Consumer } from './types';
42
+ import type { SchemaItem, SchemaSourceInfo, SchemaRelationshipCollection, VersionDiff, Owner, Producer } from './types';
43
+
44
+ const MESSAGE_TYPE_LABELS: Partial<Record<SchemaItem['collection'], string>> = {
45
+ events: 'Event',
46
+ commands: 'Command',
47
+ queries: 'Query',
48
+ };
49
+
50
+ const SCHEMA_SOURCE_PROVIDER_LABELS: Record<string, string> = {
51
+ git: 'Git',
52
+ http: 'HTTP',
53
+ };
54
+
55
+ const getSchemaSourceLabel = (source: SchemaSourceInfo) => {
56
+ const provider = SCHEMA_SOURCE_PROVIDER_LABELS[source.provider] ?? source.provider;
57
+ return source.ref ? `${provider} (${source.ref})` : provider;
58
+ };
59
+
60
+ const formatSchemaUpdatedAt = (value: string) => {
61
+ const date = new Date(value);
62
+ if (Number.isNaN(date.getTime())) return value;
63
+ return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
64
+ };
65
+
66
+ type MetadataRow = { label: string; value: React.ReactNode; title?: string };
67
+
68
+ const MetadataEmpty = () => <span className="text-[rgb(var(--ec-page-text-muted)/0.6)]">-</span>;
69
+
70
+ const MetadataTable = ({ title, rows }: { title: string; rows: MetadataRow[] }) => (
71
+ <section className="overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)] dark:border-white/10">
72
+ <div className="border-b border-[rgb(var(--ec-page-border)/0.6)] bg-[rgb(var(--ec-content-hover)/0.45)] px-4 py-2.5 dark:border-white/10">
73
+ <h3 className="text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">{title}</h3>
74
+ </div>
75
+ <dl className="divide-y divide-[rgb(var(--ec-page-border)/0.5)] dark:divide-white/8">
76
+ {rows.map((row) => (
77
+ <div key={row.label} className="grid grid-cols-[10rem_1fr] gap-4 px-4 py-2.5 text-sm" title={row.title}>
78
+ <dt className="text-[rgb(var(--ec-page-text-muted))]">{row.label}</dt>
79
+ <dd className="min-w-0 break-words font-medium text-[rgb(var(--ec-page-text))]">{row.value}</dd>
80
+ </div>
81
+ ))}
82
+ </dl>
83
+ </section>
84
+ );
85
+
86
+ const formatSchemaDateTime = (value: string) => {
87
+ const date = new Date(value);
88
+ if (Number.isNaN(date.getTime())) return value;
89
+ return date.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
90
+ };
91
+
92
+ const SchemaMetadata = ({ message, owners }: { message: SchemaItem; owners: Owner[] }) => {
93
+ const { color } = getCollectionStyles(message.collection);
94
+ const source = message.source;
95
+ const schemaRows: MetadataRow[] = [
96
+ { label: 'Name', value: message.data.name },
97
+ ...(message.schemaName && message.schemaName !== message.data.name ? [{ label: 'Schema', value: message.schemaName }] : []),
98
+ { label: 'Format', value: getSchemaTypeLabel(message.schemaExtension) },
99
+ {
100
+ label: MESSAGE_TYPE_LABELS[message.collection] ? 'Message Type' : 'Resource',
101
+ value: (
102
+ <span className={`capitalize text-${color}-500`}>{MESSAGE_TYPE_LABELS[message.collection] ?? message.collection}</span>
103
+ ),
104
+ },
105
+ { label: 'Version', value: <span className="font-mono text-xs tabular-nums">v{message.data.version}</span> },
106
+ ...(message.data.schemaPath
107
+ ? [{ label: 'File', value: <span className="font-mono text-xs">{message.data.schemaPath}</span> }]
108
+ : []),
109
+ {
110
+ label: 'Created',
111
+ value: source?.createdAt ? formatSchemaDateTime(source.createdAt) : <MetadataEmpty />,
112
+ },
113
+ {
114
+ label: 'Updated',
115
+ value: source?.updatedAt ? formatSchemaDateTime(source.updatedAt) : <MetadataEmpty />,
116
+ },
117
+ ];
118
+
119
+ const sourceRows: MetadataRow[] = source
120
+ ? [
121
+ { label: 'Source', value: getSchemaSourceLabel(source) },
122
+ ...(message.schemaRef
123
+ ? [{ label: 'Reference', value: <span className="font-mono text-xs">{message.schemaRef}</span> }]
124
+ : []),
125
+ ...(source.path ? [{ label: 'Location', value: <span className="font-mono text-xs">{source.path}</span> }] : []),
126
+ ...(source.repository
127
+ ? [{ label: 'Repository', value: <span className="font-mono text-xs">{source.repository}</span> }]
128
+ : []),
129
+ ...(source.commit ? [{ label: 'Commit', value: <span className="font-mono text-xs">{source.commit}</span> }] : []),
130
+ ...(source.url
131
+ ? [
132
+ {
133
+ label: 'Link',
134
+ value: (
135
+ <a
136
+ href={source.url}
137
+ target="_blank"
138
+ rel="noopener noreferrer"
139
+ className="inline-flex items-center gap-1 text-[rgb(var(--ec-accent))] hover:underline"
140
+ >
141
+ <span className="break-all">{source.url}</span>
142
+ <ArrowTopRightOnSquareIcon className="h-3 w-3 flex-shrink-0" />
143
+ </a>
144
+ ),
145
+ },
146
+ ]
147
+ : []),
148
+ ]
149
+ : [{ label: 'Source', value: 'Local file' }];
150
+
151
+ const ownerRows: MetadataRow[] = [
152
+ {
153
+ label: owners.length === 1 ? 'Owner' : 'Owners',
154
+ value:
155
+ owners.length > 0 ? (
156
+ <span className="flex flex-wrap gap-x-3 gap-y-1">
157
+ {owners.map((owner, idx) => (
158
+ <a key={`${owner.id}-${idx}`} href={owner.href} className="hover:text-[rgb(var(--ec-accent))] hover:underline">
159
+ {owner.name}
160
+ </a>
161
+ ))}
162
+ </span>
163
+ ) : (
164
+ <MetadataEmpty />
165
+ ),
166
+ },
167
+ ];
168
+
169
+ return (
170
+ <div className="h-full space-y-4 overflow-auto pr-1">
171
+ <MetadataTable title="Schema" rows={schemaRows} />
172
+ <MetadataTable title="Source" rows={sourceRows} />
173
+ <MetadataTable title="Ownership" rows={ownerRows} />
174
+ </div>
175
+ );
176
+ };
177
+
178
+ const SchemaVersionsTable = ({
179
+ versions,
180
+ currentVersion,
181
+ onSelect,
182
+ onCompare,
183
+ }: {
184
+ versions: SchemaItem[];
185
+ currentVersion: string;
186
+ onSelect: (version: string) => void;
187
+ /** Opens a diff between the given version and the one being viewed. */
188
+ onCompare?: (version: string) => void;
189
+ }) => (
190
+ <div className="overflow-auto rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] bg-[rgb(var(--ec-dropdown-bg)/0.66)] dark:border-white/10">
191
+ <table className="min-w-full divide-y divide-[rgb(var(--ec-page-border)/0.62)] dark:divide-white/10">
192
+ <thead className="sticky top-0 z-10 bg-[rgb(var(--ec-content-hover)/0.45)]">
193
+ <tr>
194
+ <th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
195
+ Version
196
+ </th>
197
+ <th className="w-full px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
198
+ Summary
199
+ </th>
200
+ <th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
201
+ Format
202
+ </th>
203
+ <th className="px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
204
+ Updated
205
+ </th>
206
+ {onCompare && <th className="px-4 py-2.5" />}
207
+ </tr>
208
+ </thead>
209
+ <tbody className="divide-y divide-[rgb(var(--ec-page-border)/0.5)] dark:divide-white/8">
210
+ {versions.map((version, idx) => {
211
+ const isCurrent = version.data.version === currentVersion;
212
+ const isLatest = idx === 0;
213
+ return (
214
+ <tr
215
+ key={`${version.data.version}-${idx}`}
216
+ onClick={() => onSelect(version.data.version)}
217
+ className={`group cursor-pointer transition-colors ${
218
+ isCurrent
219
+ ? 'bg-[rgb(var(--ec-accent-subtle)/0.55)]'
220
+ : 'bg-transparent hover:bg-[rgb(var(--ec-content-hover)/0.38)]'
221
+ }`}
222
+ >
223
+ <td className="whitespace-nowrap px-4 py-3 text-sm">
224
+ <button
225
+ type="button"
226
+ onClick={(event) => {
227
+ event.stopPropagation();
228
+ onSelect(version.data.version);
229
+ }}
230
+ className="inline-flex items-center gap-2 text-left"
231
+ >
232
+ <span
233
+ className={`h-2 w-2 flex-shrink-0 rounded-full ${
234
+ isCurrent
235
+ ? 'bg-[rgb(var(--ec-accent))]'
236
+ : 'bg-[rgb(var(--ec-page-border))] group-hover:bg-[rgb(var(--ec-page-text-muted))]'
237
+ }`}
238
+ />
239
+ <span className="font-mono text-xs tabular-nums text-[rgb(var(--ec-page-text))]">v{version.data.version}</span>
240
+ {isLatest && (
241
+ <span className="rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 text-[10px] font-medium text-[rgb(var(--ec-accent))]">
242
+ latest
243
+ </span>
244
+ )}
245
+ {isCurrent && <span className="text-[10px] font-medium text-[rgb(var(--ec-page-text-muted))]">viewing</span>}
246
+ </button>
247
+ </td>
248
+ <td className="px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
249
+ {version.data.summary ? <p className="line-clamp-2 max-w-2xl">{version.data.summary}</p> : <span>-</span>}
250
+ </td>
251
+ <td className="whitespace-nowrap px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
252
+ {getSchemaTypeLabel(version.schemaExtension)}
253
+ </td>
254
+ <td className="whitespace-nowrap px-4 py-3 text-sm text-[rgb(var(--ec-page-text-muted))]">
255
+ {version.source?.updatedAt ? formatSchemaUpdatedAt(version.source.updatedAt) : '-'}
256
+ </td>
257
+ {onCompare && (
258
+ <td className="whitespace-nowrap px-4 py-3 text-right text-xs">
259
+ {!isCurrent && (
260
+ <button
261
+ type="button"
262
+ onClick={(event) => {
263
+ event.stopPropagation();
264
+ onCompare(version.data.version);
265
+ }}
266
+ className="font-medium text-[rgb(var(--ec-accent))] hover:underline"
267
+ >
268
+ Compare with v{currentVersion}
269
+ </button>
270
+ )}
271
+ </td>
272
+ )}
273
+ </tr>
274
+ );
275
+ })}
276
+ </tbody>
277
+ </table>
278
+ </div>
279
+ );
280
+
281
+ type SchemaTabId = 'code' | 'schema' | 'api' | 'examples' | 'usage' | 'flows' | 'versions' | 'metadata';
282
+
283
+ /** Values used in the `tab` query parameter so a tab can be linked to directly. */
284
+ const SCHEMA_TAB_SLUGS: Record<SchemaTabId, string> = {
285
+ code: 'schema',
286
+ schema: 'properties',
287
+ metadata: 'details',
288
+ versions: 'versions',
289
+ examples: 'examples',
290
+ usage: 'producers-consumers',
291
+ flows: 'flows',
292
+ api: 'api',
293
+ };
294
+
295
+ const DEFAULT_SCHEMA_TAB: SchemaTabId = 'code';
296
+
297
+ const getTabIdFromSlug = (slug: string | null): SchemaTabId | undefined =>
298
+ (Object.keys(SCHEMA_TAB_SLUGS) as SchemaTabId[]).find((id) => SCHEMA_TAB_SLUGS[id] === slug);
299
+
300
+ const writeTabToUrl = (tab: SchemaTabId) => {
301
+ if (typeof window === 'undefined') return;
302
+ const url = new URL(window.location.href);
303
+ if (tab === DEFAULT_SCHEMA_TAB) url.searchParams.delete('tab');
304
+ else url.searchParams.set('tab', SCHEMA_TAB_SLUGS[tab]);
305
+ window.history.replaceState(window.history.state, '', url);
306
+ };
31
307
 
32
308
  interface SchemaDetailsPanelProps {
33
309
  message: SchemaItem;
@@ -37,6 +313,10 @@ interface SchemaDetailsPanelProps {
37
313
  apiAccessEnabled?: boolean;
38
314
  showOwners?: boolean;
39
315
  showProducersConsumers?: boolean;
316
+ /** Tab slug from the URL, when the server could read it. Lets the first render show the right tab. */
317
+ initialTab?: string;
318
+ /** Examples already rendered on the server (MDX with EventCatalog components). Falls back to Markdown rendering when absent. */
319
+ renderedExamples?: React.ReactNode;
40
320
  }
41
321
 
42
322
  export default function SchemaDetailsPanel({
@@ -47,26 +327,29 @@ export default function SchemaDetailsPanel({
47
327
  apiAccessEnabled = false,
48
328
  showOwners = true,
49
329
  showProducersConsumers = true,
330
+ initialTab,
331
+ renderedExamples,
50
332
  }: SchemaDetailsPanelProps) {
51
333
  const [loadDetails] = useState(createSchemaDetailsLoader);
52
334
  const content = useSchemaDetails(metadataMessage, loadDetails);
53
335
  const message = content.message!;
54
336
  const [copiedId, setCopiedId] = useState<string | null>(null);
55
- const [activeTab, setActiveTab] = useState<'code' | 'schema' | 'diff' | 'api' | 'examples'>('code');
337
+ const [activeTab, setActiveTab] = useState<SchemaTabId>(() => getTabIdFromSlug(initialTab ?? null) ?? DEFAULT_SCHEMA_TAB);
56
338
  const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
57
339
  const [isCodeModalOpen, setIsCodeModalOpen] = useState(false);
58
340
  const [isSchemaViewerModalOpen, setIsSchemaViewerModalOpen] = useState(false);
59
341
  const [isActionsOpen, setIsActionsOpen] = useState(false);
60
342
  const actionsMenuRef = useRef<HTMLDivElement>(null);
343
+ const [isVersionMenuOpen, setIsVersionMenuOpen] = useState(false);
344
+ const versionMenuRef = useRef<HTMLDivElement>(null);
61
345
 
62
- const hasMultipleVersions = availableVersions.length > 1;
63
346
  const { color } = getCollectionStyles(message.collection);
64
347
  const ext = message.schemaExtension?.toLowerCase() || '';
65
348
  const iconSpec = ICON_SPECS[ext];
66
349
  const owners = message.data.owners || [];
67
350
  const producers = message.data.producers || [];
68
351
  const consumers = message.data.consumers || [];
69
- const filename = message.data.schemaPath?.split('/').pop() || `${message.data.id}.${ext || 'json'}`;
352
+ const flows = message.data.flows || [];
70
353
 
71
354
  const uniqueAvailableVersions = useMemo(
72
355
  () =>
@@ -75,6 +358,8 @@ export default function SchemaDetailsPanel({
75
358
  ),
76
359
  [availableVersions]
77
360
  );
361
+ // Several schemas can share one message version, so count distinct versions only.
362
+ const hasMultipleVersions = uniqueAvailableVersions.length > 1;
78
363
  const defaultToVersion = uniqueAvailableVersions[0]?.data.version || '';
79
364
  const defaultFromVersion = uniqueAvailableVersions[1]?.data.version || defaultToVersion;
80
365
  const [diffFromVersion, setDiffFromVersion] = useState(defaultFromVersion);
@@ -100,7 +385,12 @@ export default function SchemaDetailsPanel({
100
385
  () => uniqueAvailableVersions.find((version) => version.data.version === diffToVersion),
101
386
  [diffToVersion, uniqueAvailableVersions]
102
387
  );
103
- const comparing = activeTab === 'diff' || isDiffModalOpen;
388
+ const comparing = (activeTab === 'versions' && hasMultipleVersions) || isDiffModalOpen;
389
+
390
+ const compareWithCurrent = (version: string) => {
391
+ setDiffFromVersion(version);
392
+ setDiffToVersion(message.data.version);
393
+ };
104
394
  const fromContent = useSchemaDetails(diffFromMetadata, loadDetails, comparing);
105
395
  const toContent = useSchemaDetails(diffToMetadata, loadDetails, comparing);
106
396
  const diffFromItem = fromContent.message;
@@ -212,18 +502,21 @@ export default function SchemaDetailsPanel({
212
502
 
213
503
  const isCopied = copiedId === message.data.id;
214
504
  const docsUrl = buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`);
215
- const sidebarCardClass = 'rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg)/0.66)] px-4 py-4';
216
505
 
217
506
  useEffect(() => {
218
507
  const handlePointerDown = (event: MouseEvent) => {
219
508
  if (!actionsMenuRef.current?.contains(event.target as Node)) {
220
509
  setIsActionsOpen(false);
221
510
  }
511
+ if (!versionMenuRef.current?.contains(event.target as Node)) {
512
+ setIsVersionMenuOpen(false);
513
+ }
222
514
  };
223
515
 
224
516
  const handleEscape = (event: KeyboardEvent) => {
225
517
  if (event.key === 'Escape') {
226
518
  setIsActionsOpen(false);
519
+ setIsVersionMenuOpen(false);
227
520
  }
228
521
  };
229
522
 
@@ -237,21 +530,63 @@ export default function SchemaDetailsPanel({
237
530
  }, []);
238
531
 
239
532
  const hasParsedSchema = !!parsedSchema || !!parsedAvroSchema || !!parsedProtoSchema;
240
- // Build tabs
241
- const tabs: { id: string; label: string; icon: React.ReactNode }[] = [
242
- { id: 'code', label: 'Schema', icon: <CodeBracketIcon className="h-3.5 w-3.5" /> },
533
+ // Build tabs in three groups: the contract itself, who uses it, then reference material.
534
+ type SchemaTab = { id: SchemaTabId; label: string; icon: React.ReactNode; group: 'contract' | 'usage' | 'reference' };
535
+ const tabs: SchemaTab[] = [
536
+ { id: 'code', label: 'Schema', icon: <CodeBracketIcon className="h-3.5 w-3.5" />, group: 'contract' },
243
537
  ];
244
538
  if (hasParsedSchema) {
245
- tabs.push({ id: 'schema', label: 'Properties', icon: <TableCellsIcon className="h-3.5 w-3.5" /> });
539
+ tabs.push({ id: 'schema', label: 'Properties', icon: <TableCellsIcon className="h-3.5 w-3.5" />, group: 'contract' });
246
540
  }
247
541
  const examples = message.examples || [];
248
- if (examples.length > 0) {
249
- tabs.push({ id: 'examples', label: 'Usage Examples', icon: <BookOpenIcon className="h-3.5 w-3.5" /> });
542
+ if (examples.length > 0 || renderedExamples) {
543
+ tabs.push({ id: 'examples', label: 'Examples', icon: <CodeBracketSquareIcon className="h-3.5 w-3.5" />, group: 'contract' });
250
544
  }
251
- if (hasMultipleVersions) {
252
- tabs.push({ id: 'diff', label: 'Changes', icon: <ClockIcon className="h-3.5 w-3.5" /> });
545
+ const usageCount = producers.length + consumers.length;
546
+ const showUsageTab = showProducersConsumers && message.collection !== 'services' && usageCount > 0;
547
+ if (showUsageTab) {
548
+ tabs.push({
549
+ id: 'usage',
550
+ label: 'Producers & Consumers',
551
+ icon: <ServerIcon className="h-3.5 w-3.5" />,
552
+ group: 'usage',
553
+ });
253
554
  }
254
- tabs.push({ id: 'api', label: 'API', icon: <GlobeAltIcon className="h-3.5 w-3.5" /> });
555
+ const showFlowsTab = message.collection !== 'services' && flows.length > 0;
556
+ if (showFlowsTab) {
557
+ tabs.push({
558
+ id: 'flows',
559
+ label: `Flows (${flows.length})`,
560
+ icon: <ArrowsRightLeftIcon className="h-3.5 w-3.5" />,
561
+ group: 'usage',
562
+ });
563
+ }
564
+ tabs.push({
565
+ id: 'versions',
566
+ label: `Versions (${uniqueAvailableVersions.length})`,
567
+ icon: <RectangleStackIcon className="h-3.5 w-3.5" />,
568
+ group: 'reference',
569
+ });
570
+ tabs.push({ id: 'metadata', label: 'Details', icon: <InformationCircleIcon className="h-3.5 w-3.5" />, group: 'reference' });
571
+ tabs.push({ id: 'api', label: 'API', icon: <GlobeAltIcon className="h-3.5 w-3.5" />, group: 'reference' });
572
+
573
+ const availableTabIds = tabs.map((tab) => tab.id).join(',');
574
+ const selectTab = (tab: SchemaTabId) => {
575
+ setActiveTab(tab);
576
+ writeTabToUrl(tab);
577
+ };
578
+
579
+ // Restore the tab named in the URL once the available tabs are known (static builds cannot resolve it on the
580
+ // server), and fall back to the default when the requested tab does not exist for this message.
581
+ useEffect(() => {
582
+ const available = availableTabIds.split(',');
583
+ const requested = getTabIdFromSlug(new URLSearchParams(window.location.search).get('tab'));
584
+ if (requested && available.includes(requested)) {
585
+ setActiveTab(requested);
586
+ } else {
587
+ setActiveTab((current) => (available.includes(current) ? current : DEFAULT_SCHEMA_TAB));
588
+ }
589
+ }, [availableTabIds]);
255
590
 
256
591
  return (
257
592
  <div className="flex h-full min-h-0 bg-[rgb(var(--ec-page-bg))] overflow-hidden">
@@ -270,14 +605,54 @@ export default function SchemaDetailsPanel({
270
605
  )}
271
606
  </div>
272
607
  <h2 className="text-xl font-semibold text-[rgb(var(--ec-page-text))] truncate">{message.data.name}</h2>
273
- <span className="flex-shrink-0 text-xs font-mono tabular-nums text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2 py-0.5 rounded-md">
274
- v{message.data.version}
275
- </span>
276
- <span
277
- className={`flex-shrink-0 inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[11px] font-medium bg-${color}-500/10 text-${color}-400 capitalize`}
278
- >
279
- {message.collection}
280
- </span>
608
+ <div className="relative flex-shrink-0" ref={versionMenuRef}>
609
+ <button
610
+ type="button"
611
+ onClick={() => setIsVersionMenuOpen((prev) => !prev)}
612
+ aria-expanded={isVersionMenuOpen}
613
+ aria-haspopup="listbox"
614
+ title="Switch version"
615
+ className="inline-flex items-center gap-1 rounded-md border border-transparent bg-[rgb(var(--ec-content-hover))] px-2 py-0.5 font-mono text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))] transition-colors hover:border-[rgb(var(--ec-page-border))] hover:text-[rgb(var(--ec-page-text))]"
616
+ >
617
+ v{message.data.version}
618
+ <ChevronDownIcon className={`h-3 w-3 transition-transform ${isVersionMenuOpen ? 'rotate-180' : ''}`} />
619
+ </button>
620
+ {isVersionMenuOpen && (
621
+ <ul
622
+ role="listbox"
623
+ aria-label="Versions"
624
+ className="absolute left-0 top-[calc(100%+0.4rem)] z-20 max-h-72 min-w-[12rem] overflow-y-auto rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] py-1 shadow-xl"
625
+ >
626
+ {uniqueAvailableVersions.map((version, idx) => {
627
+ const isCurrent = version.data.version === message.data.version;
628
+ return (
629
+ <li key={`${version.data.version}-${idx}`} role="option" aria-selected={isCurrent}>
630
+ <button
631
+ type="button"
632
+ onClick={() => {
633
+ setIsVersionMenuOpen(false);
634
+ if (!isCurrent) onVersionChange(version.data.version);
635
+ }}
636
+ className={`flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-[rgb(var(--ec-content-hover))] ${
637
+ isCurrent ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text-muted))]'
638
+ }`}
639
+ >
640
+ <CheckIcon
641
+ className={`h-3.5 w-3.5 flex-shrink-0 ${isCurrent ? 'text-[rgb(var(--ec-accent))]' : 'invisible'}`}
642
+ />
643
+ <span className="font-mono tabular-nums">v{version.data.version}</span>
644
+ {idx === 0 && (
645
+ <span className="ml-auto rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 text-[10px] font-medium text-[rgb(var(--ec-accent))]">
646
+ latest
647
+ </span>
648
+ )}
649
+ </button>
650
+ </li>
651
+ );
652
+ })}
653
+ </ul>
654
+ )}
655
+ </div>
281
656
  </div>
282
657
  {message.data.summary && (
283
658
  <p className="mt-3 text-sm leading-relaxed text-[rgb(var(--ec-page-text-muted))]">{message.data.summary}</p>
@@ -349,19 +724,24 @@ export default function SchemaDetailsPanel({
349
724
  {/* Tabs */}
350
725
  <div className="flex-shrink-0 px-6">
351
726
  <div className="flex items-center gap-1 border-b border-[rgb(var(--ec-page-border))]">
352
- {tabs.map((tab) => (
353
- <button
354
- key={tab.id}
355
- onClick={() => setActiveTab(tab.id as typeof activeTab)}
356
- className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 transition-colors ${
357
- activeTab === tab.id
358
- ? 'border-[rgb(var(--ec-accent))] text-[rgb(var(--ec-page-text))]'
359
- : 'border-transparent text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] hover:border-[rgb(var(--ec-page-border))]'
360
- }`}
361
- >
362
- {tab.icon}
363
- {tab.label}
364
- </button>
727
+ {tabs.map((tab, index) => (
728
+ <Fragment key={tab.id}>
729
+ {index > 0 && tabs[index - 1].group !== tab.group && (
730
+ <span aria-hidden="true" className="mx-2 h-4 w-px bg-[rgb(var(--ec-page-border))]" />
731
+ )}
732
+ <button
733
+ onClick={() => selectTab(tab.id)}
734
+ data-tab-group={tab.group}
735
+ className={`inline-flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 transition-colors ${
736
+ activeTab === tab.id
737
+ ? 'border-[rgb(var(--ec-accent))] text-[rgb(var(--ec-page-text))]'
738
+ : 'border-transparent text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] hover:border-[rgb(var(--ec-page-border))]'
739
+ }`}
740
+ >
741
+ {tab.icon}
742
+ {tab.label}
743
+ </button>
744
+ </Fragment>
365
745
  ))}
366
746
  </div>
367
747
  </div>
@@ -370,8 +750,98 @@ export default function SchemaDetailsPanel({
370
750
  <div className="flex-1 min-h-0 overflow-hidden p-6">
371
751
  {content.loading || content.error ? (
372
752
  <SchemaLoadingState loading={content.loading} error={content.error} retry={content.retry} />
753
+ ) : activeTab === 'examples' && renderedExamples ? (
754
+ <div className="h-full overflow-auto pr-1">{renderedExamples}</div>
373
755
  ) : activeTab === 'examples' && examples.length > 0 ? (
374
756
  <ExamplesViewer examples={examples} />
757
+ ) : activeTab === 'metadata' ? (
758
+ <SchemaMetadata message={message} owners={showOwners ? owners : []} />
759
+ ) : activeTab === 'versions' ? (
760
+ <div className="h-full space-y-4 overflow-auto pr-1">
761
+ <SchemaVersionsTable
762
+ versions={uniqueAvailableVersions}
763
+ currentVersion={message.data.version}
764
+ onSelect={onVersionChange}
765
+ onCompare={hasMultipleVersions ? compareWithCurrent : undefined}
766
+ />
767
+ {hasMultipleVersions && (
768
+ <div className="flex flex-col">
769
+ <div className="mb-4 flex flex-shrink-0 flex-col gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3 sm:flex-row sm:items-end sm:justify-between">
770
+ <div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
771
+ <label className="flex flex-col gap-1.5">
772
+ <span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
773
+ From
774
+ </span>
775
+ <select
776
+ value={diffFromVersion}
777
+ onChange={(event) => setDiffFromVersion(event.target.value)}
778
+ className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
779
+ >
780
+ {uniqueAvailableVersions.map((version) => (
781
+ <option key={`from-${version.data.version}`} value={version.data.version}>
782
+ v{version.data.version}
783
+ </option>
784
+ ))}
785
+ </select>
786
+ </label>
787
+ <label className="flex flex-col gap-1.5">
788
+ <span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
789
+ To
790
+ </span>
791
+ <select
792
+ value={diffToVersion}
793
+ onChange={(event) => setDiffToVersion(event.target.value)}
794
+ className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
795
+ >
796
+ {uniqueAvailableVersions.map((version) => (
797
+ <option key={`to-${version.data.version}`} value={version.data.version}>
798
+ v{version.data.version}
799
+ </option>
800
+ ))}
801
+ </select>
802
+ </label>
803
+ </div>
804
+ <div className="flex items-center gap-2">
805
+ <button
806
+ type="button"
807
+ onClick={() => setIsDiffModalOpen(true)}
808
+ disabled={!selectedDiff}
809
+ className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))] disabled:cursor-not-allowed disabled:opacity-50"
810
+ >
811
+ <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5" />
812
+ Expand
813
+ </button>
814
+ </div>
815
+ </div>
816
+ <div className="min-h-[16rem] overflow-hidden rounded-lg border border-[rgb(var(--ec-page-border))]">
817
+ {fromContent.loading || toContent.loading || fromContent.error || toContent.error ? (
818
+ <SchemaLoadingState
819
+ loading={fromContent.loading || toContent.loading}
820
+ error={fromContent.error || toContent.error}
821
+ retry={() => {
822
+ fromContent.retry();
823
+ toContent.retry();
824
+ }}
825
+ />
826
+ ) : diffFromVersion === diffToVersion ? (
827
+ <div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
828
+ <p className="text-sm">Select two different versions</p>
829
+ </div>
830
+ ) : !hasDiffFromContent || !hasDiffToContent ? (
831
+ <div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
832
+ <p className="text-sm">No schema content available</p>
833
+ </div>
834
+ ) : (
835
+ <DiffViewer diffs={selectedDiffs} apiAccessEnabled={apiAccessEnabled} />
836
+ )}
837
+ </div>
838
+ </div>
839
+ )}
840
+ </div>
841
+ ) : activeTab === 'usage' && showUsageTab ? (
842
+ <SchemaUsage message={message} />
843
+ ) : activeTab === 'flows' && showFlowsTab ? (
844
+ <SchemaFlows message={message} flows={flows} />
375
845
  ) : activeTab === 'api' ? (
376
846
  <ApiContentViewer
377
847
  message={message}
@@ -379,76 +849,6 @@ export default function SchemaDetailsPanel({
379
849
  copiedId={copiedId}
380
850
  apiAccessEnabled={apiAccessEnabled}
381
851
  />
382
- ) : activeTab === 'diff' && hasMultipleVersions ? (
383
- <div className="flex h-full min-h-0 flex-col overflow-hidden">
384
- <div className="mb-4 flex flex-shrink-0 flex-col gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3 sm:flex-row sm:items-end sm:justify-between">
385
- <div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
386
- <label className="flex flex-col gap-1.5">
387
- <span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
388
- From
389
- </span>
390
- <select
391
- value={diffFromVersion}
392
- onChange={(event) => setDiffFromVersion(event.target.value)}
393
- className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
394
- >
395
- {uniqueAvailableVersions.map((version) => (
396
- <option key={`from-${version.data.version}`} value={version.data.version}>
397
- v{version.data.version}
398
- </option>
399
- ))}
400
- </select>
401
- </label>
402
- <label className="flex flex-col gap-1.5">
403
- <span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
404
- To
405
- </span>
406
- <select
407
- value={diffToVersion}
408
- onChange={(event) => setDiffToVersion(event.target.value)}
409
- className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
410
- >
411
- {uniqueAvailableVersions.map((version) => (
412
- <option key={`to-${version.data.version}`} value={version.data.version}>
413
- v{version.data.version}
414
- </option>
415
- ))}
416
- </select>
417
- </label>
418
- </div>
419
- <button
420
- type="button"
421
- onClick={() => setIsDiffModalOpen(true)}
422
- disabled={!selectedDiff}
423
- className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))] disabled:cursor-not-allowed disabled:opacity-50"
424
- >
425
- <ArrowTopRightOnSquareIcon className="h-3.5 w-3.5" />
426
- Expand
427
- </button>
428
- </div>
429
- <div className="min-h-0 flex-1 overflow-hidden">
430
- {fromContent.loading || toContent.loading || fromContent.error || toContent.error ? (
431
- <SchemaLoadingState
432
- loading={fromContent.loading || toContent.loading}
433
- error={fromContent.error || toContent.error}
434
- retry={() => {
435
- fromContent.retry();
436
- toContent.retry();
437
- }}
438
- />
439
- ) : diffFromVersion === diffToVersion ? (
440
- <div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
441
- <p className="text-sm">Select two different versions</p>
442
- </div>
443
- ) : !hasDiffFromContent || !hasDiffToContent ? (
444
- <div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
445
- <p className="text-sm">No schema content available</p>
446
- </div>
447
- ) : (
448
- <DiffViewer diffs={selectedDiffs} apiAccessEnabled={apiAccessEnabled} />
449
- )}
450
- </div>
451
- </div>
452
852
  ) : (
453
853
  <SchemaContentViewer
454
854
  message={message}
@@ -471,144 +871,6 @@ export default function SchemaDetailsPanel({
471
871
  </div>
472
872
  </div>
473
873
 
474
- {/* Right sidebar - spans full height */}
475
- <div className="h-full w-80 flex-shrink-0 overflow-y-auto border-l border-[rgb(var(--ec-page-border))] px-3 py-4">
476
- <div className="space-y-3">
477
- {/* Details section */}
478
- <div className={sidebarCardClass}>
479
- <h3 className="mb-4 text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Details</h3>
480
- <dl className="space-y-2.5">
481
- <div className="flex items-center justify-between">
482
- <dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Format</dt>
483
- <dd className="text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
484
- {getSchemaTypeLabel(message.schemaExtension)}
485
- </dd>
486
- </div>
487
- <div className="flex items-center justify-between">
488
- <dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Resource</dt>
489
- <dd className={`text-xs font-medium text-${color}-400 capitalize`}>{message.collection}</dd>
490
- </div>
491
- <div className="flex items-center justify-between gap-3">
492
- <dt className="text-xs text-[rgb(var(--ec-page-text-muted))]">Filename</dt>
493
- <dd className="max-w-[11rem] truncate text-xs font-medium text-[rgb(var(--ec-page-text-muted))]" title={filename}>
494
- {filename}
495
- </dd>
496
- </div>
497
- </dl>
498
- </div>
499
-
500
- {/* Versions section */}
501
- <div className={sidebarCardClass}>
502
- <div className="mb-3 flex items-center justify-between gap-3">
503
- <h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Versions</h3>
504
- </div>
505
- <div className="space-y-1">
506
- {availableVersions
507
- .filter((v, idx, arr) => arr.findIndex((a) => a.data.version === v.data.version) === idx)
508
- .map((v, idx) => {
509
- const isActive = v.data.version === message.data.version;
510
- const isLatest = idx === 0;
511
- return (
512
- <button
513
- key={`${v.data.version}-${idx}`}
514
- onClick={() => onVersionChange(v.data.version)}
515
- className={`w-full flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors ${
516
- isActive
517
- ? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-page-text))]'
518
- : 'text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-bg)/0.55)]'
519
- }`}
520
- >
521
- <div
522
- className={`w-2 h-2 rounded-full flex-shrink-0 ${
523
- isActive ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'
524
- }`}
525
- />
526
- <span className="text-xs font-mono tabular-nums">v{v.data.version}</span>
527
- {isLatest && (
528
- <span className="text-[10px] font-medium text-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 rounded">
529
- latest
530
- </span>
531
- )}
532
- </button>
533
- );
534
- })}
535
- </div>
536
- </div>
537
-
538
- {/* Schema producers section */}
539
- {showProducersConsumers && producers.length > 0 && message.collection !== 'services' && (
540
- <div className={sidebarCardClass}>
541
- <div className="mb-2.5 flex items-center justify-between gap-3">
542
- <h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">
543
- Schema Producers ({producers.length})
544
- </h3>
545
- </div>
546
- <div className="space-y-1">
547
- {producers.map((producer: Producer, idx: number) => {
548
- const { id: serviceName, version } = getSchemaRelationshipReference(producer);
549
- return (
550
- <a
551
- key={`${producer.id}-${idx}`}
552
- href={buildUrl(`/docs/services/${serviceName}/${version}`)}
553
- className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
554
- >
555
- <ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
556
- {serviceName}
557
- </a>
558
- );
559
- })}
560
- </div>
561
- </div>
562
- )}
563
-
564
- {/* Schema consumers section */}
565
- {showProducersConsumers && consumers.length > 0 && message.collection !== 'services' && (
566
- <div className={sidebarCardClass}>
567
- <div className="mb-2.5 flex items-center justify-between gap-3">
568
- <h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">
569
- Schema Consumers ({consumers.length})
570
- </h3>
571
- </div>
572
- <div className="space-y-1">
573
- {consumers.map((consumer: Consumer, idx: number) => {
574
- const { id: serviceName, version } = getSchemaRelationshipReference(consumer);
575
- return (
576
- <a
577
- key={`${consumer.id}-${idx}`}
578
- href={buildUrl(`/docs/services/${serviceName}/${version}`)}
579
- className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
580
- >
581
- <ServerIcon className="h-3.5 w-3.5 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" />
582
- {serviceName}
583
- </a>
584
- );
585
- })}
586
- </div>
587
- </div>
588
- )}
589
-
590
- {/* Owners section */}
591
- {showOwners && owners.length > 0 && (
592
- <div className={sidebarCardClass}>
593
- <div className="mb-3 flex items-center justify-between gap-3">
594
- <h3 className="text-[0.8rem] font-semibold text-[rgb(var(--ec-page-text))]">Owners</h3>
595
- </div>
596
- <div className="space-y-1">
597
- {owners.map((owner: Owner, idx: number) => (
598
- <a
599
- key={`${owner.id}-${idx}`}
600
- href={owner.href}
601
- className="block rounded-lg px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.55)] hover:text-[rgb(var(--ec-accent))]"
602
- >
603
- {owner.name}
604
- </a>
605
- ))}
606
- </div>
607
- </div>
608
- )}
609
- </div>
610
- </div>
611
-
612
874
  {/* Modals */}
613
875
  <VersionHistoryModal
614
876
  isOpen={isDiffModalOpen}