@eventcatalog/core 3.30.0 → 3.31.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 (69) 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/{chunk-6UG4JMUV.js → chunk-7IGMIOQF.js} +1 -1
  6. package/dist/{chunk-Z26P4PCB.js → chunk-HVOLSUC2.js} +1 -1
  7. package/dist/{chunk-RRBDF4MM.js → chunk-LWVHWR77.js} +1 -1
  8. package/dist/{chunk-MVZKHUX2.js → chunk-QIJOBQZ7.js} +1 -1
  9. package/dist/{chunk-ATRBVTJ6.js → chunk-UY5QDWK7.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/astro.config.mjs +10 -6
  19. package/eventcatalog/public/logo.png +0 -0
  20. package/eventcatalog/src/components/CopyAsMarkdown.tsx +29 -24
  21. package/eventcatalog/src/components/MDX/Design/Design.astro +1 -1
  22. package/eventcatalog/src/components/MDX/Tiles/Tile.astro +11 -8
  23. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +218 -0
  24. package/eventcatalog/src/components/Settings/BillingSettingsForm.tsx +265 -0
  25. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +371 -0
  26. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +183 -0
  27. package/eventcatalog/src/components/Settings/LogoUpload.tsx +137 -0
  28. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +91 -0
  29. package/eventcatalog/src/components/Settings/ReadOnlyBanner.tsx +18 -0
  30. package/eventcatalog/src/components/Settings/Row.tsx +59 -0
  31. package/eventcatalog/src/components/Settings/SettingsShared.tsx +176 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +17 -18
  33. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +45 -16
  34. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +2 -2
  35. package/eventcatalog/src/content.config.ts +1 -1
  36. package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +11 -7
  37. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +4 -4
  38. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +70 -57
  39. package/eventcatalog/src/enterprise/feature.ts +2 -1
  40. package/eventcatalog/src/layouts/SettingsLayout.astro +116 -0
  41. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +62 -23
  42. package/eventcatalog/src/pages/_index.astro +250 -255
  43. package/eventcatalog/src/pages/api/settings/ai.ts +57 -0
  44. package/eventcatalog/src/pages/api/settings/general.ts +71 -0
  45. package/eventcatalog/src/pages/api/settings/logo.ts +113 -0
  46. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -1
  47. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +26 -32
  48. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
  49. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +40 -31
  50. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -1
  51. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +2 -26
  52. package/eventcatalog/src/pages/docs/llm/llms.txt.ts +5 -1
  53. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -1
  54. package/eventcatalog/src/pages/settings/assistant.astro +37 -0
  55. package/eventcatalog/src/pages/settings/billing.astro +17 -0
  56. package/eventcatalog/src/pages/settings/general.astro +32 -0
  57. package/eventcatalog/src/pages/settings/index.astro +21 -0
  58. package/eventcatalog/src/pages/settings/llm-access.astro +34 -0
  59. package/eventcatalog/src/pages/settings/mcp.astro +14 -0
  60. package/eventcatalog/src/styles/theme.css +38 -29
  61. package/eventcatalog/src/styles/themes/forest.css +17 -9
  62. package/eventcatalog/src/styles/themes/ocean.css +10 -2
  63. package/eventcatalog/src/styles/themes/sapphire.css +10 -2
  64. package/eventcatalog/src/styles/themes/sunset.css +25 -17
  65. package/eventcatalog/src/utils/eventcatalog-config/config-schema.ts +49 -0
  66. package/eventcatalog/src/utils/eventcatalog-config/config-writer.ts +149 -0
  67. package/eventcatalog/src/utils/url-builder.ts +4 -2
  68. package/package.json +7 -5
  69. package/eventcatalog/src/pages/docs/llm/llms-services.txt.ts +0 -81
@@ -0,0 +1,91 @@
1
+ import { ExternalLink, Server, ServerCog } from 'lucide-react';
2
+ import { Row } from './Row';
3
+ import { LiveCard, MCP_DOCS_URL, UpgradeRequired, UrlPanel } from './SettingsShared';
4
+
5
+ interface Props {
6
+ hasScalePlan: boolean;
7
+ inSSR: boolean;
8
+ mcpUrl: string;
9
+ }
10
+
11
+ export const McpSettingsForm = ({ hasScalePlan, inSSR, mcpUrl }: Props) => {
12
+ const mcpAvailable = hasScalePlan && inSSR;
13
+
14
+ return (
15
+ <div className="divide-y divide-[rgb(var(--ec-page-border))]">
16
+ <Row
17
+ title="MCP Server"
18
+ description="Expose your catalog over the Model Context Protocol so AI agents (Claude Desktop, Cursor, etc.) can query your architecture as a live data source."
19
+ canEdit={false}
20
+ dirty={false}
21
+ >
22
+ {mcpAvailable ? (
23
+ <McpAvailable url={mcpUrl} />
24
+ ) : !hasScalePlan ? (
25
+ <UpgradeRequired
26
+ tier="Scale"
27
+ blurb="The MCP Server is a Scale-plan feature. Upgrade to expose your catalog to AI agents over the Model Context Protocol."
28
+ docsUrl={MCP_DOCS_URL}
29
+ />
30
+ ) : (
31
+ <McpNeedsSSR />
32
+ )}
33
+ </Row>
34
+ </div>
35
+ );
36
+ };
37
+
38
+ const McpAvailable = ({ url }: { url: string }) => (
39
+ <div className="space-y-3">
40
+ <LiveCard
41
+ icon={<Server className="h-4 w-4" aria-hidden />}
42
+ title="MCP server is live"
43
+ description="Point Claude Desktop, Cursor, or any MCP-aware client at the endpoint below."
44
+ />
45
+ <div className="rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg)/0.4)] px-4 py-3">
46
+ <p className="text-[12px] font-medium text-[rgb(var(--ec-page-text))]">Endpoint</p>
47
+ <UrlPanel url={url} />
48
+ <a
49
+ href={MCP_DOCS_URL}
50
+ target="_blank"
51
+ rel="noreferrer"
52
+ className="mt-3 inline-flex items-center gap-1 text-[12px] font-medium text-[rgb(var(--ec-accent))] hover:underline"
53
+ >
54
+ Connect a client
55
+ <ExternalLink className="h-3 w-3" aria-hidden />
56
+ </a>
57
+ </div>
58
+ </div>
59
+ );
60
+
61
+ const McpNeedsSSR = () => (
62
+ <div className="overflow-hidden rounded-lg border border-[rgb(var(--ec-accent)/0.4)] bg-gradient-to-br from-[rgb(var(--ec-accent)/0.1)] via-[rgb(var(--ec-accent)/0.05)] to-transparent">
63
+ <div className="flex items-start gap-3 px-4 py-3.5">
64
+ <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-[rgb(var(--ec-accent)/0.4)] bg-[rgb(var(--ec-accent)/0.1)] text-[rgb(var(--ec-accent))]">
65
+ <ServerCog className="h-4 w-4" aria-hidden />
66
+ </span>
67
+ <div className="flex-1 min-w-0">
68
+ <div className="flex items-center gap-2">
69
+ <p className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">Server output mode required</p>
70
+ <span className="inline-flex items-center gap-1 rounded-full border border-[rgb(var(--ec-accent)/0.4)] bg-[rgb(var(--ec-accent)/0.1)] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-accent))]">
71
+ SSR
72
+ </span>
73
+ </div>
74
+ <p className="mt-1 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
75
+ The MCP Server requires your catalog to run in server (SSR) mode. Follow the setup guide to switch.
76
+ </p>
77
+ <div className="mt-3 flex flex-wrap items-center gap-3">
78
+ <a
79
+ href={MCP_DOCS_URL}
80
+ target="_blank"
81
+ rel="noreferrer"
82
+ className="inline-flex items-center gap-1 rounded-md bg-[rgb(var(--ec-accent))] px-3 py-1.5 text-[12px] font-semibold text-white shadow-sm transition-colors hover:bg-[rgb(var(--ec-accent-hover))]"
83
+ >
84
+ MCP setup guide
85
+ <ExternalLink className="h-3 w-3" aria-hidden />
86
+ </a>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ );
@@ -0,0 +1,18 @@
1
+ import { Lock } from 'lucide-react';
2
+
3
+ export const ReadOnlyBanner = () => (
4
+ <div
5
+ role="status"
6
+ className="mb-6 flex items-start gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] p-4 text-sm text-[rgb(var(--ec-page-text))]"
7
+ >
8
+ <Lock className="mt-0.5 h-4 w-4 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" aria-hidden />
9
+ <div className="space-y-1">
10
+ <p className="font-medium">Read-only</p>
11
+ <p className="text-[rgb(var(--ec-page-text-muted))]">
12
+ To edit these settings, run EventCatalog locally or update them in your{' '}
13
+ <code className="rounded bg-[rgb(var(--ec-page-bg)/0.78)] px-1 py-0.5 font-mono text-xs">eventcatalog.config.js</code>{' '}
14
+ file.
15
+ </p>
16
+ </div>
17
+ </div>
18
+ );
@@ -0,0 +1,59 @@
1
+ import { AlertCircle } from 'lucide-react';
2
+
3
+ export const cn = (...parts: Array<string | false | undefined | null>): string => parts.filter(Boolean).join(' ');
4
+
5
+ export const inputBase =
6
+ 'block w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg,var(--ec-page-bg)))] px-3 py-2 text-[13px] text-[rgb(var(--ec-page-text))] placeholder:text-[rgb(var(--ec-page-text-muted)/0.7)] transition-colors focus:border-[rgb(var(--ec-accent)/0.6)] focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.25)] disabled:cursor-not-allowed disabled:opacity-60';
7
+
8
+ export const monoInput = 'font-mono text-[12.5px] tracking-tight';
9
+
10
+ export const inputError = 'border-red-500/70 focus:border-red-500 focus:ring-red-500/20';
11
+
12
+ interface RowProps {
13
+ title: string;
14
+ description: string;
15
+ canEdit: boolean;
16
+ dirty: boolean;
17
+ saving?: boolean;
18
+ onSave?: () => void;
19
+ error?: string;
20
+ children: React.ReactNode;
21
+ }
22
+
23
+ export const Row = ({ title, description, canEdit, dirty, saving, onSave, error, children }: RowProps) => (
24
+ <section className="grid grid-cols-1 gap-6 py-8 md:grid-cols-[minmax(0,_18rem)_minmax(0,_1fr)] md:gap-12">
25
+ <header className="space-y-1.5">
26
+ <h3 className="text-[14px] font-semibold tracking-tight text-[rgb(var(--ec-page-text))]">{title}</h3>
27
+ <p className="text-[13px] leading-relaxed text-[rgb(var(--ec-content-text-muted,var(--ec-page-text-muted)))]">
28
+ {description}
29
+ </p>
30
+ </header>
31
+ <div className="space-y-2.5">
32
+ {children}
33
+ {error && (
34
+ <p className="flex items-center gap-1.5 text-[12px] text-red-500">
35
+ <AlertCircle className="h-3 w-3 flex-shrink-0" aria-hidden />
36
+ {error}
37
+ </p>
38
+ )}
39
+ {onSave && canEdit && (
40
+ <div className="pt-1">
41
+ <button
42
+ type="button"
43
+ onClick={onSave}
44
+ disabled={!dirty || saving}
45
+ className={cn(
46
+ 'inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-[12px] font-medium transition-colors',
47
+ dirty && !saving
48
+ ? 'border-[rgb(var(--ec-accent)/0.5)] bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.8)]'
49
+ : 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text-muted))]',
50
+ 'disabled:cursor-not-allowed disabled:opacity-60'
51
+ )}
52
+ >
53
+ {saving ? 'Saving…' : 'Save changes'}
54
+ </button>
55
+ </div>
56
+ )}
57
+ </div>
58
+ </section>
59
+ );
@@ -0,0 +1,176 @@
1
+ import { useState } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { ExternalLink, Lock, Sparkles, Copy, Check as CheckIcon } from 'lucide-react';
4
+ import { cn } from './Row';
5
+
6
+ export const ASSISTANT_DOCS_URL =
7
+ 'https://www.eventcatalog.dev/docs/development/ask-your-architecture/eventcatalog-assistant/what-is-eventcatalog-assistant';
8
+ export const ASSISTANT_CONFIGURATION_DOCS_URL =
9
+ 'https://www.eventcatalog.dev/docs/development/ask-your-architecture/eventcatalog-assistant/configuration';
10
+ export const MCP_DOCS_URL = 'https://www.eventcatalog.dev/docs/development/ask-your-architecture/mcp-server/getting-started';
11
+ export const PRICING_URL = 'https://www.eventcatalog.dev/pricing';
12
+
13
+ interface UpgradeRequiredProps {
14
+ tier: string;
15
+ blurb: string;
16
+ docsUrl?: string;
17
+ }
18
+
19
+ export const UpgradeRequired = ({ tier, blurb, docsUrl }: UpgradeRequiredProps) => (
20
+ <div className="overflow-hidden rounded-lg border border-amber-500/40 bg-gradient-to-br from-amber-500/10 via-amber-500/5 to-transparent">
21
+ <div className="flex items-start gap-3 px-4 py-3.5">
22
+ <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-amber-500/40 bg-amber-500/10 text-amber-500">
23
+ <Lock className="h-4 w-4" aria-hidden />
24
+ </span>
25
+ <div className="flex-1 min-w-0">
26
+ <div className="flex items-center gap-2">
27
+ <p className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">Available on {tier}</p>
28
+ <span className="inline-flex items-center gap-1 rounded-full border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-500">
29
+ <Sparkles className="h-2.5 w-2.5" aria-hidden />
30
+ Upgrade
31
+ </span>
32
+ </div>
33
+ <p className="mt-1 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">{blurb}</p>
34
+ <div className="mt-3 flex flex-wrap items-center gap-3">
35
+ <a
36
+ href={PRICING_URL}
37
+ target="_blank"
38
+ rel="noreferrer"
39
+ className="inline-flex items-center gap-1 rounded-md bg-amber-500 px-3 py-1.5 text-[12px] font-semibold text-white shadow-sm transition-colors hover:bg-amber-600"
40
+ >
41
+ View plans
42
+ <ExternalLink className="h-3 w-3" aria-hidden />
43
+ </a>
44
+ {docsUrl && (
45
+ <a
46
+ href={docsUrl}
47
+ target="_blank"
48
+ rel="noreferrer"
49
+ className="inline-flex items-center gap-1 text-[12px] font-medium text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
50
+ >
51
+ Learn more
52
+ <ExternalLink className="h-3 w-3" aria-hidden />
53
+ </a>
54
+ )}
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+
61
+ interface UrlPanelProps {
62
+ url: string;
63
+ }
64
+
65
+ export const UrlPanel = ({ url }: UrlPanelProps) => {
66
+ const [copied, setCopied] = useState(false);
67
+ const copy = async () => {
68
+ try {
69
+ await navigator.clipboard.writeText(new URL(url, window.location.origin).href);
70
+ setCopied(true);
71
+ setTimeout(() => setCopied(false), 1600);
72
+ } catch {
73
+ toast.error('Could not copy URL');
74
+ }
75
+ };
76
+ return (
77
+ <div className="mt-2 flex items-center gap-2">
78
+ <a
79
+ href={url}
80
+ target="_blank"
81
+ rel="noreferrer"
82
+ className="flex-1 truncate rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-2.5 py-1.5 font-mono text-[12px] text-[rgb(var(--ec-page-text))] transition-colors hover:border-[rgb(var(--ec-accent)/0.5)] hover:text-[rgb(var(--ec-accent))]"
83
+ >
84
+ {url}
85
+ </a>
86
+ <button
87
+ type="button"
88
+ onClick={copy}
89
+ aria-label="Copy URL"
90
+ className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]"
91
+ >
92
+ {copied ? <CheckIcon className="h-3.5 w-3.5 text-green-500" aria-hidden /> : <Copy className="h-3.5 w-3.5" aria-hidden />}
93
+ </button>
94
+ <a
95
+ href={url}
96
+ target="_blank"
97
+ rel="noreferrer"
98
+ aria-label="Open in new tab"
99
+ className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-bg)/0.78)] hover:text-[rgb(var(--ec-page-text))]"
100
+ >
101
+ <ExternalLink className="h-3.5 w-3.5" aria-hidden />
102
+ </a>
103
+ </div>
104
+ );
105
+ };
106
+
107
+ interface LiveCardProps {
108
+ icon: React.ReactNode;
109
+ title: string;
110
+ description: string;
111
+ }
112
+
113
+ export const LiveCard = ({ icon, title, description }: LiveCardProps) => (
114
+ <div className="flex items-center gap-3 rounded-lg border border-emerald-500/30 bg-emerald-500/5 px-4 py-3">
115
+ <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-emerald-500/30 bg-emerald-500/10 text-emerald-500">
116
+ {icon}
117
+ </span>
118
+ <div className="flex-1 min-w-0">
119
+ <p className="text-[13px] font-medium text-[rgb(var(--ec-page-text))]">{title}</p>
120
+ <p className="text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">{description}</p>
121
+ </div>
122
+ <span className="flex-shrink-0 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-500">
123
+ Live
124
+ </span>
125
+ </div>
126
+ );
127
+
128
+ interface ToggleProps {
129
+ checked: boolean;
130
+ disabled?: boolean;
131
+ onChange: (v: boolean) => void;
132
+ }
133
+
134
+ export const Toggle = ({ checked, disabled, onChange }: ToggleProps) => (
135
+ <button
136
+ type="button"
137
+ role="switch"
138
+ aria-checked={checked}
139
+ disabled={disabled}
140
+ onClick={() => onChange(!checked)}
141
+ className={cn(
142
+ 'relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer items-center rounded-full transition-colors',
143
+ checked ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-text-muted)/0.35)]',
144
+ disabled && 'cursor-not-allowed opacity-50'
145
+ )}
146
+ >
147
+ <span
148
+ className={cn(
149
+ 'inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow-sm transition-transform',
150
+ checked ? 'translate-x-[1.125rem]' : 'translate-x-[0.1875rem]'
151
+ )}
152
+ />
153
+ </button>
154
+ );
155
+
156
+ interface ToggleRowProps {
157
+ icon: React.ReactNode;
158
+ label: string;
159
+ hint: string;
160
+ checked: boolean;
161
+ disabled?: boolean;
162
+ onChange: (v: boolean) => void;
163
+ }
164
+
165
+ export const ToggleRow = ({ icon, label, hint, checked, disabled, onChange }: ToggleRowProps) => (
166
+ <div className="flex items-center gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg,var(--ec-page-bg)))] px-4 py-3">
167
+ <span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text-muted))]">
168
+ {icon}
169
+ </span>
170
+ <div className="flex-1 min-w-0">
171
+ <p className="text-[13px] font-medium text-[rgb(var(--ec-page-text))]">{label}</p>
172
+ <p className="text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">{hint}</p>
173
+ </div>
174
+ <Toggle checked={checked} disabled={disabled} onChange={onChange} />
175
+ </div>
176
+ );
@@ -501,9 +501,9 @@ export default function NestedSideBar() {
501
501
  // Show loading state if no data yet
502
502
  if (!data || roots.length === 0) {
503
503
  return (
504
- <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-page-bg))]">
504
+ <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-rail-bg))]">
505
505
  {/* Search skeleton */}
506
- <div className="px-4 py-3 border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-page-bg))]">
506
+ <div className="px-4 py-3 border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-rail-bg))]">
507
507
  <div className="h-10 bg-[rgb(var(--ec-content-hover))] rounded-xl animate-pulse" />
508
508
  </div>
509
509
  {/* Content skeleton */}
@@ -749,10 +749,9 @@ export default function NestedSideBar() {
749
749
  )}
750
750
  <span
751
751
  className={cn(
752
- 'tracking-tight',
753
752
  isSubtleGroup
754
- ? 'text-[11px] text-[rgb(var(--ec-content-text-muted))] font-medium'
755
- : 'text-[12px] text-[rgb(var(--ec-content-text))] font-semibold'
753
+ ? 'text-[10px] font-semibold uppercase tracking-[0.12em] text-[rgb(var(--ec-content-text-muted))]'
754
+ : 'text-[12px] font-semibold tracking-tight text-[rgb(var(--ec-content-text))]'
756
755
  )}
757
756
  >
758
757
  {group.title}
@@ -771,7 +770,7 @@ export default function NestedSideBar() {
771
770
  );
772
771
 
773
772
  return (
774
- <div key={`group-${groupKey || index}`} className={cn(isSubtleGroup ? 'mb-3 last:mb-1' : 'mb-5 last:mb-2')}>
773
+ <div key={`group-${groupKey || index}`} className={cn(isSubtleGroup ? 'mb-2 last:mb-1' : 'mb-5 last:mb-2')}>
775
774
  {canCollapse ? (
776
775
  <button
777
776
  onClick={() => toggleSectionCollapse(groupId)}
@@ -793,7 +792,7 @@ export default function NestedSideBar() {
793
792
  <div
794
793
  className={cn(
795
794
  'flex flex-col gap-0.5 border-[rgb(var(--ec-content-border))]',
796
- isSubtleGroup ? 'border-l ml-3 mt-0.5' : shouldFlattenSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
795
+ isSubtleGroup ? 'mt-0.5' : shouldFlattenSubtleChildren ? 'mt-1' : 'border-l ml-4 mt-1'
797
796
  )}
798
797
  >
799
798
  {visibleChildren.map((childRef, childIndex) => {
@@ -896,11 +895,9 @@ export default function NestedSideBar() {
896
895
  );
897
896
 
898
897
  const baseClasses =
899
- 'group flex items-center justify-between w-full px-3 py-2 border border-transparent cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] active:bg-[rgb(var(--ec-content-hover))]';
898
+ 'group flex items-center justify-between w-full px-3 py-1.5 border border-transparent cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))] active:bg-[rgb(var(--ec-content-hover))]';
900
899
  const parentClasses = itemHasChildren ? 'font-medium' : '';
901
- const activeClasses = isActive
902
- ? 'border-[rgb(var(--ec-accent)/0.16)] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle))] shadow-sm'
903
- : '';
900
+ const activeClasses = isActive ? 'bg-[rgb(var(--ec-rail-active-bg))] hover:bg-[rgb(var(--ec-rail-active-bg))]' : '';
904
901
 
905
902
  // Leaf item with href → render as link
906
903
  if (item.href && !itemHasChildren) {
@@ -938,18 +935,18 @@ export default function NestedSideBar() {
938
935
  };
939
936
 
940
937
  return (
941
- <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-page-bg))]">
938
+ <aside className="w-full min-h-full flex-1 flex flex-col font-sans bg-[rgb(var(--ec-rail-bg))]">
942
939
  {isTopLevel && (
943
- <div className="px-4 py-[13.5px] bg-[rgb(var(--ec-page-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
944
- <div className="flex items-center w-full px-2 py-1.5">
945
- <span className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] truncate">All resources</span>
946
- </div>
940
+ <div className="flex h-[60px] items-center px-6 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
941
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text)/0.5)] truncate">
942
+ All resources
943
+ </span>
947
944
  </div>
948
945
  )}
949
946
 
950
947
  {!isTopLevel && (
951
948
  <div
952
- className="px-4 py-[13.5px] bg-[rgb(var(--ec-page-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10"
949
+ className="flex h-[60px] items-center px-4 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10"
953
950
  onMouseEnter={() => !isTopLevel && setShowPathPreview(true)}
954
951
  onMouseLeave={() => {
955
952
  setShowPathPreview(false);
@@ -974,7 +971,9 @@ export default function NestedSideBar() {
974
971
  >
975
972
  <ChevronLeft className="w-4 h-4" />
976
973
  </span>
977
- <span className="text-[12px] font-semibold text-[rgb(var(--ec-content-text))] truncate">{currentLevel.title}</span>
974
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text))] truncate">
975
+ {currentLevel.title}
976
+ </span>
978
977
  {currentLevel.badge && (
979
978
  <span
980
979
  className={cn(
@@ -11,7 +11,7 @@ import {
11
11
  } from '@tanstack/react-table';
12
12
  import { ChevronLeft, ChevronRight, SearchX, X, Search, Users } from 'lucide-react';
13
13
  import { UserIcon } from '@heroicons/react/24/outline';
14
- import { useMemo, useState } from 'react';
14
+ import { useEffect, useMemo, useState } from 'react';
15
15
  import type { TableConfiguration } from '@types';
16
16
  import { isSameVersion } from '@utils/collections/version-compare';
17
17
  import { FilterDropdown, CheckboxItem } from './FilterComponents';
@@ -110,6 +110,18 @@ export function DiscoverTable<T extends DiscoverTableData>({
110
110
  );
111
111
  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
112
112
  const [tableFilter, setTableFilter] = useState('');
113
+ const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
114
+ const PAGE_SIZE_STORAGE_KEY = 'eventcatalog-discover-page-size';
115
+ const [pageSize, setPageSize] = useState<number>(() => {
116
+ if (typeof window === 'undefined') return 10;
117
+ const stored = Number(window.localStorage.getItem(PAGE_SIZE_STORAGE_KEY));
118
+ return PAGE_SIZE_OPTIONS.includes(stored) ? stored : 10;
119
+ });
120
+ useEffect(() => {
121
+ if (typeof window !== 'undefined') {
122
+ window.localStorage.setItem(PAGE_SIZE_STORAGE_KEY, String(pageSize));
123
+ }
124
+ }, [pageSize]);
113
125
  const [showOnlyLatest, setShowOnlyLatest] = useState(true);
114
126
  const [onlyShowDrafts, setOnlyShowDrafts] = useState(false);
115
127
  const [selectedDomains, setSelectedDomains] = useState<string[]>([]);
@@ -304,8 +316,15 @@ export function DiscoverTable<T extends DiscoverTableData>({
304
316
  columnFilters,
305
317
  columnVisibility,
306
318
  },
319
+ initialState: {
320
+ pagination: { pageIndex: 0, pageSize },
321
+ },
307
322
  });
308
323
 
324
+ useEffect(() => {
325
+ table.setPageSize(pageSize);
326
+ }, [pageSize, table]);
327
+
309
328
  const totalResults = table.getPrePaginationRowModel().rows.length;
310
329
  const hasResults = table.getRowModel().rows.length > 0;
311
330
 
@@ -433,11 +452,13 @@ export function DiscoverTable<T extends DiscoverTableData>({
433
452
  <div className="flex h-full min-h-0" style={{ ['--ec-discover-sidebar-width' as any]: '320px' }}>
434
453
  {/* Filter Sidebar */}
435
454
  <div
436
- className="fixed left-[var(--ec-vertical-nav-width)] top-0 z-20 flex h-screen w-[320px] flex-shrink-0 flex-col border-r border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-page-bg))]"
455
+ className="fixed left-[var(--ec-vertical-nav-width)] top-0 z-20 flex h-screen w-[320px] flex-shrink-0 flex-col border-r border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-rail-bg))]"
437
456
  style={{ width: 'var(--ec-discover-sidebar-width, 320px)' }}
438
457
  >
439
458
  <div className="flex h-[60px] flex-shrink-0 items-center border-b border-[rgb(var(--ec-page-border))] px-4">
440
- <h2 className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">{collectionLabel} Filters</h2>
459
+ <h2 className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text)/0.5)]">
460
+ {collectionLabel} Filters
461
+ </h2>
441
462
  </div>
442
463
 
443
464
  {/* Filter sections */}
@@ -445,10 +466,6 @@ export function DiscoverTable<T extends DiscoverTableData>({
445
466
  {/* Message Filters Section */}
446
467
  {(showProducersFilter || showConsumersFilter) && (filteredProducers.length > 0 || filteredConsumers.length > 0) && (
447
468
  <div className="space-y-3">
448
- <h3 className="text-[11px] font-bold uppercase tracking-widest text-[rgb(var(--ec-page-text))] pb-2">
449
- Message Filters
450
- </h3>
451
-
452
469
  {/* Producers Filter */}
453
470
  {showProducersFilter && filteredProducers.length > 0 && (
454
471
  <div>
@@ -679,10 +696,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
679
696
  {/* Table Header */}
680
697
  <div className="flex items-end justify-between gap-6 px-6 py-5">
681
698
  <div className="min-w-0">
682
- <h2 className="text-2xl font-semibold text-[rgb(var(--ec-page-text))] md:text-4xl">
683
- {collectionLabel}{' '}
684
- <span className="ml-1 text-lg font-normal text-[rgb(var(--ec-page-text-muted))] md:text-3xl">({totalResults})</span>
685
- </h2>
699
+ <h2 className="text-2xl font-semibold text-[rgb(var(--ec-page-text))] md:text-4xl">{collectionLabel}</h2>
686
700
  {collectionDescription && (
687
701
  <p className="max-w-3xl pt-2 text-base font-light text-[rgb(var(--ec-page-text-muted))]">{collectionDescription}</p>
688
702
  )}
@@ -694,7 +708,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
694
708
  value={tableFilter}
695
709
  onChange={(e) => setTableFilter(e.target.value)}
696
710
  placeholder="Filter..."
697
- className="w-64 rounded-lg border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] py-2 pl-9 pr-3 text-sm text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-icon-color))] transition-colors focus:border-[rgb(var(--ec-accent))] focus:outline-hidden focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
711
+ className="w-64 rounded-md border-0 bg-[rgb(var(--ec-page-bg))] py-1.5 pl-9 pr-3 text-sm font-light text-[rgb(var(--ec-header-text))] shadow-xs ring-1 ring-inset ring-[rgb(var(--ec-dropdown-border))] placeholder:text-[rgb(var(--ec-icon-color))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
698
712
  />
699
713
  {tableFilter && (
700
714
  <button
@@ -766,14 +780,29 @@ export function DiscoverTable<T extends DiscoverTableData>({
766
780
 
767
781
  {/* Pagination */}
768
782
  <div className="flex-shrink-0 flex items-center justify-between px-6 py-3 border-t border-[rgb(var(--ec-page-border))]">
769
- <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
783
+ <div className="flex items-center gap-3 text-xs text-[rgb(var(--ec-page-text-muted))]">
770
784
  {totalResults > 0 && (
771
- <>
785
+ <span>
772
786
  <span className="font-medium text-[rgb(var(--ec-page-text))]">{table.getRowModel().rows.length}</span> of{' '}
773
787
  <span className="font-medium text-[rgb(var(--ec-page-text))]">{totalResults}</span> results
774
- </>
788
+ </span>
775
789
  )}
776
- </span>
790
+ {totalResults > 0 && <span aria-hidden className="h-3 w-px bg-[rgb(var(--ec-page-border))]" />}
791
+ <label className="flex items-center gap-1.5">
792
+ <span>Per page</span>
793
+ <select
794
+ value={pageSize}
795
+ onChange={(e) => setPageSize(Number(e.target.value))}
796
+ 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"
797
+ >
798
+ {PAGE_SIZE_OPTIONS.map((size) => (
799
+ <option key={size} value={size}>
800
+ {size}
801
+ </option>
802
+ ))}
803
+ </select>
804
+ </label>
805
+ </div>
777
806
  <div className="flex items-center gap-1.5">
778
807
  <button
779
808
  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"
@@ -45,7 +45,7 @@ export const FilterDropdown = ({ label, selectedItems, onClear, onRemoveItem, ch
45
45
  setIsOpen(!isOpen);
46
46
  }
47
47
  }}
48
- className={`w-full flex items-center justify-between px-3 py-2 text-sm rounded-lg border transition-colors cursor-pointer ${
48
+ className={`w-full flex items-center justify-between px-3 py-2 text-xs rounded-lg border transition-colors cursor-pointer ${
49
49
  hasSelection || isOpen
50
50
  ? 'border-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent)/0.05)]'
51
51
  : 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg))] hover:border-[rgb(var(--ec-icon-color))]'
@@ -152,7 +152,7 @@ export const CheckboxItem = ({ label, checked, onChange, count, icon }: Checkbox
152
152
  </div>
153
153
  {icon && <span className="flex-shrink-0 text-[rgb(var(--ec-icon-color))]">{icon}</span>}
154
154
  <span
155
- className={`text-sm flex-1 truncate ${checked ? 'font-medium text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text))]'}`}
155
+ className={`text-xs flex-1 truncate ${checked ? 'font-medium text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text))]'}`}
156
156
  >
157
157
  {label}
158
158
  </span>
@@ -32,7 +32,7 @@ export const projectDirBase = (() => {
32
32
 
33
33
  const withIgnoredBuildArtifacts = (patterns: string | string[]) => {
34
34
  if (process.env.IGNORE_BUILD_ARTIFACTS === 'true') {
35
- return [...patterns, '!dist/**'];
35
+ return Array.isArray(patterns) ? [...patterns, '!dist/**'] : [patterns, '!dist/**'];
36
36
  }
37
37
  return patterns;
38
38
  };
@@ -80,6 +80,16 @@ export function findMatchingRule(rules: Record<string, () => boolean>, pathname:
80
80
  return matches.length > 0 ? matches[0].rule : null;
81
81
  }
82
82
 
83
+ export function getPublicRoutes(isLLMSTextEnabled: boolean) {
84
+ const publicRoutes = ['/auth/login', '/auth/signout', '/auth/error', '/api/auth'];
85
+
86
+ if (!isLLMSTextEnabled) {
87
+ return publicRoutes;
88
+ }
89
+
90
+ return [...publicRoutes, '/docs/llm/llms.txt', '/docs/llm/llms-full.txt', '/docs/llm/schemas.txt'];
91
+ }
92
+
83
93
  export const authMiddleware: MiddlewareHandler = async (context, next) => {
84
94
  const { request, redirect, locals } = context;
85
95
  const url = new URL(request.url);
@@ -97,13 +107,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
97
107
 
98
108
  // Skip system/browser requests
99
109
  const systemRoutes = ['/.well-known/', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/_astro/', '/__astro'];
100
- let publicRoutes = ['/auth/login', '/auth/signout', '/auth/error', '/api/auth'];
101
-
102
- const llmsRoutes = ['/docs/llm/llms.txt', '/docs/llm/llms-services.txt', '/docs/llm/llms-full.txt'];
103
-
104
- if (isLLMSTextEnabled) {
105
- publicRoutes = [...publicRoutes, ...llmsRoutes];
106
- }
110
+ const publicRoutes = getPublicRoutes(isLLMSTextEnabled);
107
111
 
108
112
  if (
109
113
  pathname.startsWith('/_') ||
@@ -204,14 +204,14 @@ const CustomDocsNav: React.FC<CustomDocsNavProps> = ({ sidebarItems }) => {
204
204
 
205
205
  return (
206
206
  <nav ref={navRef} className="flex h-full min-h-0 flex-col text-[rgb(var(--ec-page-text))]">
207
- <div className="flex-shrink-0 border-b border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 py-3.5">
208
- <div className="flex gap-2">
207
+ <div className="flex h-[60px] flex-shrink-0 items-center border-b border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-rail-bg))] px-3">
208
+ <div className="flex w-full gap-2">
209
209
  <input
210
210
  type="text"
211
211
  value={searchTerm}
212
212
  onChange={handleSearchChange}
213
213
  placeholder="Quick search..."
214
- className="flex-1 p-2 px-2 text-sm rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-input-placeholder))] h-[30px]"
214
+ className="flex-1 min-w-0 p-2 px-2 text-sm rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-icon-color))] h-[30px] focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:border-[rgb(var(--ec-accent))]"
215
215
  />
216
216
  <button
217
217
  onClick={toggleExpandCollapse}
@@ -228,7 +228,7 @@ const CustomDocsNav: React.FC<CustomDocsNavProps> = ({ sidebarItems }) => {
228
228
  </div>
229
229
 
230
230
  <div className="min-h-0 flex-1 overflow-y-auto">
231
- <div className="space-y-2 divide-y divide-[rgb(var(--ec-page-border))] pb-4 pt-2">
231
+ <div className="space-y-2 pb-4 pt-2">
232
232
  {hasNoResults ? (
233
233
  <NoResultsFound searchTerm={debouncedSearchTerm} />
234
234
  ) : (