@eventcatalog/core 3.30.0 → 3.31.2

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 (70) 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-Z26P4PCB.js → chunk-5VANHNV3.js} +1 -1
  6. package/dist/{chunk-RRBDF4MM.js → chunk-7FECQ5B3.js} +1 -1
  7. package/dist/{chunk-MVZKHUX2.js → chunk-DL3PF5MS.js} +1 -1
  8. package/dist/{chunk-6UG4JMUV.js → chunk-IPGFRHRL.js} +1 -1
  9. package/dist/{chunk-ATRBVTJ6.js → chunk-UOKUSIKW.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/SchemaExplorer/AvroSchemaViewer.tsx +25 -18
  24. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +218 -0
  25. package/eventcatalog/src/components/Settings/BillingSettingsForm.tsx +265 -0
  26. package/eventcatalog/src/components/Settings/GeneralSettingsForm.tsx +371 -0
  27. package/eventcatalog/src/components/Settings/LlmAccessSettingsForm.tsx +183 -0
  28. package/eventcatalog/src/components/Settings/LogoUpload.tsx +137 -0
  29. package/eventcatalog/src/components/Settings/McpSettingsForm.tsx +91 -0
  30. package/eventcatalog/src/components/Settings/ReadOnlyBanner.tsx +18 -0
  31. package/eventcatalog/src/components/Settings/Row.tsx +59 -0
  32. package/eventcatalog/src/components/Settings/SettingsShared.tsx +176 -0
  33. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +17 -18
  34. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +45 -16
  35. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +2 -2
  36. package/eventcatalog/src/content.config.ts +1 -1
  37. package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +11 -7
  38. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +4 -4
  39. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +70 -57
  40. package/eventcatalog/src/enterprise/feature.ts +2 -1
  41. package/eventcatalog/src/layouts/SettingsLayout.astro +116 -0
  42. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +62 -23
  43. package/eventcatalog/src/pages/_index.astro +250 -255
  44. package/eventcatalog/src/pages/api/settings/ai.ts +57 -0
  45. package/eventcatalog/src/pages/api/settings/general.ts +71 -0
  46. package/eventcatalog/src/pages/api/settings/logo.ts +113 -0
  47. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +1 -1
  48. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +26 -32
  49. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
  50. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +40 -31
  51. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +1 -1
  52. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +2 -26
  53. package/eventcatalog/src/pages/docs/llm/llms.txt.ts +5 -1
  54. package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -1
  55. package/eventcatalog/src/pages/settings/assistant.astro +37 -0
  56. package/eventcatalog/src/pages/settings/billing.astro +17 -0
  57. package/eventcatalog/src/pages/settings/general.astro +32 -0
  58. package/eventcatalog/src/pages/settings/index.astro +21 -0
  59. package/eventcatalog/src/pages/settings/llm-access.astro +34 -0
  60. package/eventcatalog/src/pages/settings/mcp.astro +14 -0
  61. package/eventcatalog/src/styles/theme.css +38 -29
  62. package/eventcatalog/src/styles/themes/forest.css +17 -9
  63. package/eventcatalog/src/styles/themes/ocean.css +10 -2
  64. package/eventcatalog/src/styles/themes/sapphire.css +10 -2
  65. package/eventcatalog/src/styles/themes/sunset.css +25 -17
  66. package/eventcatalog/src/utils/eventcatalog-config/config-schema.ts +49 -0
  67. package/eventcatalog/src/utils/eventcatalog-config/config-writer.ts +149 -0
  68. package/eventcatalog/src/utils/url-builder.ts +4 -2
  69. package/package.json +7 -5
  70. package/eventcatalog/src/pages/docs/llm/llms-services.txt.ts +0 -81
@@ -0,0 +1,57 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { isDevMode } from '@utils/feature';
3
+ import { aiSettingsSchema } from '@utils/eventcatalog-config/config-schema';
4
+ import {
5
+ applyConfigUpdate,
6
+ readConfigSource,
7
+ writeConfigUpdate,
8
+ type ConfigUpdate,
9
+ } from '@utils/eventcatalog-config/config-writer';
10
+
11
+ export const prerender = !isDevMode();
12
+
13
+ const json = (status: number, body: unknown) =>
14
+ new Response(JSON.stringify(body), {
15
+ status,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ });
18
+
19
+ export const POST: APIRoute = async ({ request }) => {
20
+ if (!isDevMode()) {
21
+ return json(403, { error: 'Settings can only be edited when running in dev mode (EVENTCATALOG_DEV_MODE=true).' });
22
+ }
23
+
24
+ let body: unknown;
25
+ try {
26
+ body = await request.json();
27
+ } catch {
28
+ return json(400, { error: 'Invalid JSON body' });
29
+ }
30
+
31
+ const parsed = aiSettingsSchema.safeParse(body);
32
+ if (!parsed.success) {
33
+ return json(400, { error: 'Validation failed', issues: parsed.error.issues });
34
+ }
35
+
36
+ let source: string;
37
+ try {
38
+ source = readConfigSource();
39
+ } catch (err) {
40
+ return json(500, { error: `Could not read eventcatalog.config.js: ${(err as Error).message}` });
41
+ }
42
+
43
+ const data = parsed.data;
44
+ const update: ConfigUpdate = {
45
+ llmsTxt: { enabled: data.llmsTxtEnabled },
46
+ chat: { enabled: data.chatEnabled },
47
+ };
48
+
49
+ try {
50
+ applyConfigUpdate(source, update);
51
+ writeConfigUpdate(update);
52
+ } catch (err) {
53
+ return json(500, { error: `Could not update eventcatalog.config.js: ${(err as Error).message}` });
54
+ }
55
+
56
+ return json(200, { ok: true, settings: data });
57
+ };
@@ -0,0 +1,71 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { isDevMode } from '@utils/feature';
3
+ import { generalSettingsSchema } from '@utils/eventcatalog-config/config-schema';
4
+ import {
5
+ applyConfigUpdate,
6
+ readConfigSource,
7
+ writeConfigUpdate,
8
+ type ConfigUpdate,
9
+ } from '@utils/eventcatalog-config/config-writer';
10
+
11
+ export const prerender = !isDevMode();
12
+
13
+ const json = (status: number, body: unknown) =>
14
+ new Response(JSON.stringify(body), {
15
+ status,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ });
18
+
19
+ export const POST: APIRoute = async ({ request }) => {
20
+ if (!isDevMode()) {
21
+ return json(403, { error: 'Settings can only be edited when running in dev mode (EVENTCATALOG_DEV_MODE=true).' });
22
+ }
23
+
24
+ let body: unknown;
25
+ try {
26
+ body = await request.json();
27
+ } catch {
28
+ return json(400, { error: 'Invalid JSON body' });
29
+ }
30
+
31
+ const parsed = generalSettingsSchema.safeParse(body);
32
+ if (!parsed.success) {
33
+ return json(400, { error: 'Validation failed', issues: parsed.error.issues });
34
+ }
35
+
36
+ // Verify the config file exists and is parseable before we attempt the write.
37
+ let source: string;
38
+ try {
39
+ source = readConfigSource();
40
+ } catch (err) {
41
+ return json(500, { error: `Could not read eventcatalog.config.js: ${(err as Error).message}` });
42
+ }
43
+
44
+ const data = parsed.data;
45
+ const update: ConfigUpdate = {
46
+ title: data.title,
47
+ tagline: data.tagline ?? null,
48
+ organizationName: data.organizationName ?? null,
49
+ homepageLink: data.homepageLink ?? null,
50
+ editUrl: data.editUrl ?? null,
51
+ repositoryUrl: data.repositoryUrl ?? null,
52
+ theme: data.theme,
53
+ };
54
+
55
+ if (data.logo) {
56
+ update.logo = {
57
+ alt: data.logo.alt ?? null,
58
+ text: data.logo.text ?? null,
59
+ };
60
+ }
61
+
62
+ try {
63
+ // Dry-run the write first to surface parse errors before touching disk.
64
+ applyConfigUpdate(source, update);
65
+ writeConfigUpdate(update);
66
+ } catch (err) {
67
+ return json(500, { error: `Could not update eventcatalog.config.js: ${(err as Error).message}` });
68
+ }
69
+
70
+ return json(200, { ok: true, settings: data });
71
+ };
@@ -0,0 +1,113 @@
1
+ import type { APIRoute } from 'astro';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isDevMode } from '@utils/feature';
5
+ import { writeConfigUpdate } from '@utils/eventcatalog-config/config-writer';
6
+
7
+ export const prerender = !isDevMode();
8
+
9
+ const MAX_BYTES = 2 * 1024 * 1024;
10
+ const ALLOWED: Record<string, string> = {
11
+ 'image/png': 'png',
12
+ 'image/jpeg': 'jpg',
13
+ 'image/svg+xml': 'svg',
14
+ 'image/webp': 'webp',
15
+ };
16
+
17
+ const json = (status: number, body: unknown) =>
18
+ new Response(JSON.stringify(body), {
19
+ status,
20
+ headers: { 'Content-Type': 'application/json' },
21
+ });
22
+
23
+ const projectRoot = () => process.env.PROJECT_DIR ?? process.cwd();
24
+ const publicDir = () => path.join(projectRoot(), 'public');
25
+ const LOGO_BASENAME = 'eventcatalog-logo';
26
+
27
+ const removeExistingLogos = () => {
28
+ const dir = publicDir();
29
+ if (!fs.existsSync(dir)) return;
30
+ for (const entry of fs.readdirSync(dir)) {
31
+ if (entry.startsWith(`${LOGO_BASENAME}.`)) {
32
+ fs.unlinkSync(path.join(dir, entry));
33
+ }
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Best-effort SVG sanitization: strips <script> blocks and on* event handlers.
39
+ * Not a full sanitizer; we accept this as a v1 limitation for SVG uploads.
40
+ */
41
+ const sanitizeSvg = (svg: string): string =>
42
+ svg
43
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
44
+ .replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, '')
45
+ .replace(/\son[a-z]+\s*=\s*'[^']*'/gi, '')
46
+ .replace(/javascript:/gi, '');
47
+
48
+ export const POST: APIRoute = async ({ request }) => {
49
+ if (!isDevMode()) {
50
+ return json(403, { error: 'Logo can only be changed when running in dev mode (EVENTCATALOG_DEV_MODE=true).' });
51
+ }
52
+
53
+ let form: FormData;
54
+ try {
55
+ form = await request.formData();
56
+ } catch {
57
+ return json(400, { error: 'Expected multipart/form-data body' });
58
+ }
59
+
60
+ const file = form.get('logo');
61
+ if (!(file instanceof File)) {
62
+ return json(400, { error: 'Missing `logo` file field' });
63
+ }
64
+
65
+ const ext = ALLOWED[file.type];
66
+ if (!ext) {
67
+ return json(400, { error: `Unsupported logo type: ${file.type}. Allowed: PNG, JPG, SVG, WebP.` });
68
+ }
69
+
70
+ if (file.size > MAX_BYTES) {
71
+ return json(413, { error: `Logo exceeds 2MB size limit (got ${file.size} bytes).` });
72
+ }
73
+
74
+ const buffer = Buffer.from(await file.arrayBuffer());
75
+ const dir = publicDir();
76
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
77
+
78
+ removeExistingLogos();
79
+
80
+ const targetName = `${LOGO_BASENAME}.${ext}`;
81
+ const targetPath = path.join(dir, targetName);
82
+
83
+ if (ext === 'svg') {
84
+ const sanitized = sanitizeSvg(buffer.toString('utf8'));
85
+ fs.writeFileSync(targetPath, sanitized, 'utf8');
86
+ } else {
87
+ fs.writeFileSync(targetPath, buffer);
88
+ }
89
+
90
+ const src = `/${targetName}`;
91
+ try {
92
+ writeConfigUpdate({ logo: { src } });
93
+ } catch (err) {
94
+ return json(500, { error: `Logo saved but config update failed: ${(err as Error).message}` });
95
+ }
96
+
97
+ return json(200, { ok: true, src });
98
+ };
99
+
100
+ export const DELETE: APIRoute = async () => {
101
+ if (!isDevMode()) {
102
+ return json(403, { error: 'Logo can only be changed when running in dev mode.' });
103
+ }
104
+
105
+ removeExistingLogos();
106
+ try {
107
+ writeConfigUpdate({ logo: { src: null } });
108
+ } catch (err) {
109
+ return json(500, { error: `Logo files removed but config update failed: ${(err as Error).message}` });
110
+ }
111
+
112
+ return json(200, { ok: true });
113
+ };
@@ -129,7 +129,7 @@ const pagefindAttributes =
129
129
  )
130
130
  }
131
131
  </div>
132
- <div class="prose py-8 max-w-none">
132
+ <div class="prose py-4 max-w-none">
133
133
  <Content components={components(props)} />
134
134
  </div>
135
135
  </div>
@@ -51,12 +51,10 @@ const pagefindAttributes =
51
51
  <div class="flex docs-layout w-full">
52
52
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
53
53
  <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-2">
54
- <div>
55
- <div class="flex justify-between items-center">
56
- <div class="flex items-center gap-2">
57
- <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
58
- </div>
59
- <div class="hidden lg:block">
54
+ <div class="flex flex-col gap-4">
55
+ <div class="flex justify-between items-center gap-4">
56
+ <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
57
+ <div class="hidden lg:block shrink-0">
60
58
  <CopyAsMarkdown
61
59
  client:only="react"
62
60
  schemas={[]}
@@ -69,32 +67,28 @@ const pagefindAttributes =
69
67
  />
70
68
  </div>
71
69
  </div>
70
+ {props.data.summary && <p class="text-base text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>}
71
+ {
72
+ badges.length > 0 && (
73
+ <div class="flex flex-wrap gap-3 py-4">
74
+ {badges.map((badge: any) => (
75
+ <span
76
+ class={`
77
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
78
+ bg-[rgb(var(--ec-content-hover))] border border-[rgb(var(--ec-page-border))]
79
+ text-[rgb(var(--ec-page-text))]
80
+ `}
81
+ >
82
+ {badge.iconComponent && (
83
+ <badge.iconComponent className="w-4 h-4 flex-shrink-0 text-[rgb(var(--ec-icon-color))]" />
84
+ )}
85
+ <span>{badge.content}</span>
86
+ </span>
87
+ ))}
88
+ </div>
89
+ )
90
+ }
72
91
  </div>
73
- {
74
- props.data.summary && (
75
- <p class="text-base pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>
76
- )
77
- }
78
- {
79
- badges.length > 0 && (
80
- <div class="flex flex-wrap gap-3 py-4">
81
- {badges.map((badge: any) => (
82
- <span
83
- class={`
84
- inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
85
- bg-[rgb(var(--ec-content-hover))] border border-[rgb(var(--ec-page-border))]
86
- text-[rgb(var(--ec-page-text))]
87
- `}
88
- >
89
- {badge.iconComponent && (
90
- <badge.iconComponent className="w-4 h-4 flex-shrink-0 text-[rgb(var(--ec-icon-color))]" />
91
- )}
92
- <span>{badge.content}</span>
93
- </span>
94
- ))}
95
- </div>
96
- )
97
- }
98
92
  </div>
99
93
  <div data-pagefind-ignore>
100
94
  {
@@ -129,7 +123,7 @@ const pagefindAttributes =
129
123
  )
130
124
  }
131
125
  </div>
132
- <div class="prose py-8 max-w-none">
126
+ <div class="prose py-4 max-w-none">
133
127
  <Content components={components(props)} />
134
128
  </div>
135
129
  </div>
@@ -80,7 +80,7 @@ const pagefindAttributes =
80
80
  </div>
81
81
 
82
82
  {
83
- badges && (
83
+ badges && badges.length > 0 && (
84
84
  <div class="flex flex-wrap gap-3 py-4">
85
85
  {badges.map((badge: any) => {
86
86
  return (
@@ -248,6 +248,7 @@ const {
248
248
 
249
249
  let friendlyCollectionName = props.collection.slice(0, props.collection.length - 1);
250
250
  friendlyCollectionName = friendlyCollectionName === 'querie' ? 'query' : friendlyCollectionName;
251
+ friendlyCollectionName = friendlyCollectionName === 'entitie' ? 'entity' : friendlyCollectionName;
251
252
 
252
253
  const schemasForResource = getSchemasFromResource(props);
253
254
 
@@ -287,38 +288,46 @@ nodeGraphs.push({
287
288
  <main class="flex docs-layout min-h-full bg-[rgb(var(--ec-page-bg))]" {...pagefindAttributes}>
288
289
  <div class="flex docs-layout w-full">
289
290
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
290
- <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-2">
291
+ <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-6 pt-4">
291
292
  <div>
292
- <div class="flex justify-between items-center">
293
- <div class="flex items-center gap-2">
294
- {
295
- headerIconSrc && (
296
- <img
297
- src={headerIconSrc}
298
- alt={props.data.name}
299
- class="w-8 h-8 md:w-10 md:h-10 object-contain rounded-md shrink-0"
293
+ <div class="flex justify-between items-end">
294
+ <div class="flex flex-col gap-4">
295
+ <span class="text-xs md:text-sm font-semibold text-[rgb(var(--ec-accent))] capitalize">
296
+ {friendlyCollectionName}
297
+ </span>
298
+ <div class="flex items-center gap-3">
299
+ {
300
+ headerIconSrc && (
301
+ <img
302
+ src={headerIconSrc}
303
+ alt={props.data.name}
304
+ class="w-8 h-8 md:w-10 md:h-10 object-contain rounded-md shrink-0"
305
+ />
306
+ )
307
+ }
308
+ <div class="flex items-center gap-2">
309
+ <h2
310
+ id="doc-page-header"
311
+ class={`text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))] ${props.data.deprecated && hasDeprecated ? 'text-red-500' : ''}`}
312
+ >
313
+ {props.data.name}
314
+ {props.data.latestVersion !== props.data.version && <span>(v{props.data.version})</span>}
315
+ </h2>
316
+ <FavoriteButton
317
+ client:load
318
+ nodeKey={`${collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap]}:${props.data.id}:${props.data.version}`}
319
+ title={props.data.name}
320
+ badge={collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap]
321
+ .charAt(0)
322
+ .toUpperCase() +
323
+ collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap].slice(1)}
324
+ href={buildUrl(`/docs/${props.collection}/${props.data.id}/${props.data.version}`)}
325
+ size="md"
300
326
  />
301
- )
302
- }
303
- <h2
304
- id="doc-page-header"
305
- class={`text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))] ${props.data.deprecated && hasDeprecated ? 'text-red-500' : ''}`}
306
- >
307
- {props.data.name}
308
- {props.data.latestVersion !== props.data.version && <span>(v{props.data.version})</span>}
309
- </h2>
310
- <FavoriteButton
311
- client:load
312
- nodeKey={`${collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap]}:${props.data.id}:${props.data.version}`}
313
- title={props.data.name}
314
- badge={collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap]
315
- .charAt(0)
316
- .toUpperCase() + collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap].slice(1)}
317
- href={buildUrl(`/docs/${props.collection}/${props.data.id}/${props.data.version}`)}
318
- size="md"
319
- />
327
+ </div>
328
+ </div>
320
329
  </div>
321
- <div class="hidden lg:block">
330
+ <div class="hidden lg:block mb-2">
322
331
  <CopyAsMarkdown
323
332
  client:only="react"
324
333
  schemas={schemasForResource}
@@ -333,10 +342,10 @@ nodeGraphs.push({
333
342
  </div>
334
343
  </div>
335
344
 
336
- <h2 class="text-base pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</h2>
345
+ <h2 class="text-base pt-4 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</h2>
337
346
  {
338
347
  badges && (
339
- <div class="flex flex-wrap gap-3 py-4">
348
+ <div class="flex flex-wrap gap-3 pt-6 pb-2">
340
349
  {badges.map((badge: any) => {
341
350
  return (
342
351
  <span
@@ -77,7 +77,7 @@ const badges = [
77
77
  <p class="text-lg pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{ubiquitousLanguage.summary}</p>
78
78
  <!-- Add badge -->
79
79
  {
80
- badges && (
80
+ badges && badges.length > 0 && (
81
81
  <div class="flex flex-wrap gap-3 py-4">
82
82
  {badges.map((badge: any) => {
83
83
  return (
@@ -47,7 +47,7 @@ const { subdomains, duplicateTerms } = ubiquitousLanguageData;
47
47
  type="text"
48
48
  id="searchInput"
49
49
  placeholder="Search terms..."
50
- class="w-full px-4 py-2.5 pl-10 border border-[rgb(var(--ec-input-border))] rounded-lg focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:border-[rgb(var(--ec-accent))] text-sm bg-[rgb(var(--ec-input-bg))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-input-placeholder))] transition-colors"
50
+ class="block w-full rounded-md border-0 py-1.5 pr-4 pl-10 text-sm font-light text-[rgb(var(--ec-header-text))] bg-[rgb(var(--ec-header-bg))] shadow-xs ring-1 ring-inset ring-[rgb(var(--ec-dropdown-border))] placeholder:text-[rgb(var(--ec-icon-color))] focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:outline-none"
51
51
  />
52
52
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
53
53
  <svg class="h-4 w-4 text-[rgb(var(--ec-icon-color))]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -59,14 +59,6 @@ const { subdomains, duplicateTerms } = ubiquitousLanguageData;
59
59
  </svg>
60
60
  </div>
61
61
  </div>
62
- <div class="mt-2 text-right">
63
- <span
64
- class="inline-flex items-center px-2 py-1 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-md"
65
- id="resultsCount"
66
- >
67
- {/* This will be updated by JavaScript */}
68
- </span>
69
- </div>
70
62
  </div>
71
63
  </div>
72
64
  </div>
@@ -323,15 +315,11 @@ const { subdomains, duplicateTerms } = ubiquitousLanguageData;
323
315
  function initializeSearch() {
324
316
  const searchInput = document.getElementById('searchInput');
325
317
  const domainSections = document.querySelectorAll('[data-domain-section]');
326
- const resultsCount = document.getElementById('resultsCount');
327
318
 
328
319
  function updateResults() {
329
320
  //@ts-ignore
330
321
  const searchTerm = searchInput?.value.toLowerCase() || '';
331
- let totalVisibleTerms = 0;
332
- let totalTerms = 0;
333
322
 
334
- // Handle search for each domain section
335
323
  domainSections.forEach((section) => {
336
324
  const domainId = section.getAttribute('data-domain-section');
337
325
  const domainCards = section.querySelectorAll('.term-card');
@@ -339,19 +327,14 @@ const { subdomains, duplicateTerms } = ubiquitousLanguageData;
339
327
  let domainVisibleCount = 0;
340
328
 
341
329
  domainCards.forEach((card) => {
342
- totalTerms++;
343
330
  const title = card.querySelector('h4')?.textContent?.toLowerCase() || '';
344
331
  const description = card.querySelector('p')?.textContent?.toLowerCase() || '';
345
332
  const matches = searchTerm === '' || title.includes(searchTerm) || description.includes(searchTerm);
346
333
 
347
334
  card.classList.toggle('hidden', !matches);
348
- if (matches) {
349
- domainVisibleCount++;
350
- totalVisibleTerms++;
351
- }
335
+ if (matches) domainVisibleCount++;
352
336
  });
353
337
 
354
- // Show/hide domain-specific no results message
355
338
  if (domainNoResults) {
356
339
  if (searchTerm.trim() === '') {
357
340
  domainNoResults.classList.add('hidden');
@@ -360,16 +343,9 @@ const { subdomains, duplicateTerms } = ubiquitousLanguageData;
360
343
  }
361
344
  }
362
345
  });
363
-
364
- // Update results count
365
- if (resultsCount) {
366
- resultsCount.textContent = `Showing ${totalVisibleTerms} terms`;
367
- }
368
346
  }
369
347
 
370
348
  searchInput?.addEventListener('input', updateResults);
371
-
372
- // Initialize results count
373
349
  updateResults();
374
350
  }
375
351
 
@@ -2,7 +2,7 @@ import { getCollection } from 'astro:content';
2
2
  import config from '@config';
3
3
  import type { APIRoute } from 'astro';
4
4
 
5
- import { isCustomDocsEnabled, isResourceDocsEnabled } from '@utils/feature';
5
+ import { isCustomDocsEnabled, isResourceDocsEnabled, isLLMSTxtEnabled } from '@utils/feature';
6
6
  import { getUbiquitousLanguage } from '@utils/collections/domains';
7
7
  import { getResourceDocs } from '@utils/collections/resource-docs';
8
8
 
@@ -82,6 +82,10 @@ const renderEntities = (baseUrl: string) => {
82
82
  };
83
83
 
84
84
  export const GET: APIRoute = async ({ params, request }) => {
85
+ if (!isLLMSTxtEnabled()) {
86
+ return new Response('llms.txt is not enabled for this Catalog.', { status: 404 });
87
+ }
88
+
85
89
  const url = new URL(request.url);
86
90
  const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`;
87
91
 
@@ -74,7 +74,7 @@ const pageTitle = `User | ${props.data.name}`;
74
74
  <img
75
75
  src={props.data.avatarUrl}
76
76
  alt={`${props.data.name}'s profile picture`}
77
- class="w-20 h-20 rounded-full object-cover border-2 border-[rgb(var(--ec-page-border))]"
77
+ class="w-20 h-20 rounded-md object-cover border-2 border-[rgb(var(--ec-page-border))]"
78
78
  />
79
79
  )
80
80
  }
@@ -0,0 +1,37 @@
1
+ ---
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import SettingsLayout from '@layouts/SettingsLayout.astro';
5
+ import { AssistantSettingsForm } from '@components/Settings/AssistantSettingsForm';
6
+ import { isDevMode, isSSR, isEventCatalogStarterEnabled, isEventCatalogScaleEnabled } from '@utils/feature';
7
+ import { buildUrl } from '@utils/url-builder';
8
+ import config from '@config';
9
+
10
+ const canEdit = isDevMode();
11
+
12
+ const initial = {
13
+ llmsTxtEnabled: config?.llmsTxt?.enabled ?? true,
14
+ chatEnabled: config?.chat?.enabled ?? true,
15
+ };
16
+
17
+ const projectRoot = process.env.PROJECT_DIR ?? process.cwd();
18
+ const hasChatConfigFile = fs.existsSync(path.join(projectRoot, 'eventcatalog.chat.js'));
19
+ const hasPlan = isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled();
20
+ const inSSR = isSSR();
21
+ const chatAvailable = hasPlan && hasChatConfigFile && inSSR;
22
+
23
+ const apiBase = buildUrl('/api/settings');
24
+ ---
25
+
26
+ <SettingsLayout title="Assistant Agent" active="assistant">
27
+ <AssistantSettingsForm
28
+ client:load
29
+ canEdit={canEdit}
30
+ initial={initial}
31
+ chatAvailable={chatAvailable}
32
+ hasPlan={hasPlan}
33
+ inSSR={inSSR}
34
+ hasChatConfigFile={hasChatConfigFile}
35
+ apiBase={apiBase}
36
+ />
37
+ </SettingsLayout>
@@ -0,0 +1,17 @@
1
+ ---
2
+ import SettingsLayout from '@layouts/SettingsLayout.astro';
3
+ import { BillingSettingsForm, type PlanId } from '@components/Settings/BillingSettingsForm';
4
+ import { isEventCatalogStarterEnabled, isEventCatalogScaleEnabled } from '@utils/feature';
5
+
6
+ // Detect which plan is active. Enterprise has no env flag of its own — it includes
7
+ // Scale features, so an explicit Enterprise toggle would need a separate flag.
8
+ // For now, treat anything Scale-licensed as Scale; Enterprise users will still see
9
+ // upgrade copy on the Scale card but the Plans grid is the canonical source.
10
+ let currentPlan: PlanId = 'community';
11
+ if (isEventCatalogScaleEnabled()) currentPlan = 'scale';
12
+ else if (isEventCatalogStarterEnabled()) currentPlan = 'starter';
13
+ ---
14
+
15
+ <SettingsLayout title="Billing" active="billing">
16
+ <BillingSettingsForm client:load currentPlan={currentPlan} />
17
+ </SettingsLayout>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import SettingsLayout from '@layouts/SettingsLayout.astro';
3
+ import { GeneralSettingsForm } from '@components/Settings/GeneralSettingsForm';
4
+ import { isDevMode } from '@utils/feature';
5
+ import { buildUrl } from '@utils/url-builder';
6
+ import config from '@config';
7
+
8
+ const canEdit = isDevMode();
9
+
10
+ const initial = {
11
+ title: config?.title ?? '',
12
+ tagline: config?.tagline,
13
+ organizationName: config?.organizationName,
14
+ homepageLink: config?.homepageLink,
15
+ editUrl: config?.editUrl,
16
+ repositoryUrl: config?.repositoryUrl,
17
+ logo: config?.logo
18
+ ? {
19
+ alt: config.logo.alt,
20
+ text: config.logo.text,
21
+ src: config.logo.src,
22
+ }
23
+ : undefined,
24
+ theme: config?.theme || 'default',
25
+ };
26
+
27
+ const apiBase = buildUrl('/api/settings');
28
+ ---
29
+
30
+ <SettingsLayout title="General" active="general">
31
+ <GeneralSettingsForm client:load canEdit={canEdit} initial={initial} apiBase={apiBase} />
32
+ </SettingsLayout>
@@ -0,0 +1,21 @@
1
+ ---
2
+ import { buildUrl } from '@utils/url-builder';
3
+
4
+ const target = buildUrl('/settings/general');
5
+ ---
6
+
7
+ <!doctype html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta http-equiv="refresh" content={`0; url=${target}`} />
12
+ <link rel="canonical" href={target} />
13
+ <title>Redirecting…</title>
14
+ </head>
15
+ <body>
16
+ <p>Redirecting to <a href={target}>{target}</a>…</p>
17
+ <script is:inline define:vars={{ target }}>
18
+ window.location.replace(target);
19
+ </script>
20
+ </body>
21
+ </html>