@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,218 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Toaster, toast } from 'sonner';
3
+ import { ExternalLink, MessageSquare, ServerCog, Wrench } from 'lucide-react';
4
+ import { aiSettingsSchema } from '@utils/eventcatalog-config/config-schema';
5
+ import { ReadOnlyBanner } from './ReadOnlyBanner';
6
+ import { Row, cn } from './Row';
7
+ import { ASSISTANT_CONFIGURATION_DOCS_URL, ASSISTANT_DOCS_URL, ToggleRow, UpgradeRequired } from './SettingsShared';
8
+
9
+ interface Props {
10
+ canEdit: boolean;
11
+ initial: { chatEnabled: boolean; llmsTxtEnabled: boolean };
12
+ chatAvailable: boolean;
13
+ hasPlan: boolean;
14
+ inSSR: boolean;
15
+ hasChatConfigFile: boolean;
16
+ apiBase: string;
17
+ }
18
+
19
+ export const AssistantSettingsForm = ({ canEdit, initial, chatAvailable, hasPlan, inSSR, hasChatConfigFile, apiBase }: Props) => {
20
+ const [chatEnabled, setChatEnabled] = useState(initial.chatEnabled);
21
+ const [pristine, setPristine] = useState(initial.chatEnabled);
22
+ const [saving, setSaving] = useState(false);
23
+
24
+ const dirty = chatEnabled !== pristine;
25
+
26
+ useEffect(() => {
27
+ if (!dirty) return;
28
+ const handler = (e: BeforeUnloadEvent) => {
29
+ e.preventDefault();
30
+ e.returnValue = '';
31
+ };
32
+ window.addEventListener('beforeunload', handler);
33
+ return () => window.removeEventListener('beforeunload', handler);
34
+ }, [dirty]);
35
+
36
+ const save = async () => {
37
+ if (!canEdit) return;
38
+ const candidate = aiSettingsSchema.safeParse({
39
+ llmsTxtEnabled: initial.llmsTxtEnabled,
40
+ chatEnabled,
41
+ });
42
+ if (!candidate.success) {
43
+ toast.error('Invalid settings');
44
+ return;
45
+ }
46
+ setSaving(true);
47
+ try {
48
+ const res = await fetch(`${apiBase}/ai`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify(candidate.data),
52
+ });
53
+ const body = await res.json().catch(() => ({}));
54
+ if (!res.ok) {
55
+ toast.error(body.error || 'Could not save');
56
+ return;
57
+ }
58
+ setPristine(chatEnabled);
59
+ toast.success('Saved to eventcatalog.config.js');
60
+ } catch (err) {
61
+ toast.error(`Could not save: ${(err as Error).message}`);
62
+ } finally {
63
+ setSaving(false);
64
+ }
65
+ };
66
+
67
+ return (
68
+ <form onSubmit={(e) => e.preventDefault()} className="divide-y divide-[rgb(var(--ec-page-border))]">
69
+ <Toaster richColors closeButton position="bottom-right" theme="system" />
70
+ {!canEdit && (
71
+ <div className="pb-6">
72
+ <ReadOnlyBanner />
73
+ </div>
74
+ )}
75
+
76
+ <Row
77
+ title="Assistant Agent"
78
+ description="Assistant agent that answers questions about your architecture directly in your catalog."
79
+ canEdit={canEdit && chatAvailable}
80
+ dirty={dirty}
81
+ saving={saving}
82
+ onSave={chatAvailable ? save : undefined}
83
+ >
84
+ {chatAvailable ? (
85
+ <div className="space-y-3">
86
+ <ToggleRow
87
+ icon={<MessageSquare className="h-4 w-4" aria-hidden />}
88
+ label={chatEnabled ? 'Enabled' : 'Disabled'}
89
+ hint={chatEnabled ? 'Chat is available to readers of this catalog.' : 'Chat is hidden from the catalog.'}
90
+ checked={chatEnabled}
91
+ disabled={!canEdit}
92
+ onChange={setChatEnabled}
93
+ />
94
+ {chatEnabled && <ConfigurationRequired />}
95
+ </div>
96
+ ) : !hasPlan ? (
97
+ <UpgradeRequired
98
+ tier="Starter and Scale"
99
+ blurb="The EventCatalog Assistant is part of our paid plans. Upgrade to give your team a built-in AI agent that answers questions about your architecture."
100
+ docsUrl={ASSISTANT_DOCS_URL}
101
+ />
102
+ ) : !inSSR ? (
103
+ <AssistantNeedsSSR />
104
+ ) : !hasChatConfigFile ? (
105
+ <AssistantNeedsConfigFile />
106
+ ) : null}
107
+ </Row>
108
+ </form>
109
+ );
110
+ };
111
+
112
+ const ConfigurationRequired = () => (
113
+ <div className="rounded-lg border border-[rgb(var(--ec-accent)/0.4)] bg-[rgb(var(--ec-accent)/0.06)] px-4 py-3">
114
+ <div className="flex items-start gap-3">
115
+ <span className="flex h-7 w-7 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))]">
116
+ <Wrench className="h-3.5 w-3.5" aria-hidden />
117
+ </span>
118
+ <div className="flex-1 min-w-0">
119
+ <p className="text-[13px] font-medium text-[rgb(var(--ec-page-text))]">Configuration required</p>
120
+ <p className="mt-0.5 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
121
+ The Agent needs an{' '}
122
+ <code className="rounded bg-[rgb(var(--ec-page-bg))] px-1 py-0.5 font-mono text-[11px]">eventcatalog.chat.js</code> file
123
+ and a model provider (bring your own model). Follow the configuration guide to set it up.
124
+ </p>
125
+ <a
126
+ href={ASSISTANT_CONFIGURATION_DOCS_URL}
127
+ target="_blank"
128
+ rel="noreferrer"
129
+ className={cn(
130
+ 'mt-2 inline-flex items-center gap-1 text-[12px] font-medium text-[rgb(var(--ec-accent))] hover:underline'
131
+ )}
132
+ >
133
+ Configure the Agent
134
+ <ExternalLink className="h-3 w-3" aria-hidden />
135
+ </a>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+
141
+ const AssistantNeedsSSR = () => (
142
+ <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">
143
+ <div className="flex items-start gap-3 px-4 py-3.5">
144
+ <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))]">
145
+ <ServerCog className="h-4 w-4" aria-hidden />
146
+ </span>
147
+ <div className="flex-1 min-w-0">
148
+ <div className="flex items-center gap-2">
149
+ <p className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">Server output mode required</p>
150
+ <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))]">
151
+ SSR
152
+ </span>
153
+ </div>
154
+ <p className="mt-1 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
155
+ The Agent requires your catalog to run in server (SSR) mode. Follow the configuration guide to switch.
156
+ </p>
157
+ <div className="mt-3 flex flex-wrap items-center gap-3">
158
+ <a
159
+ href={ASSISTANT_CONFIGURATION_DOCS_URL}
160
+ target="_blank"
161
+ rel="noreferrer"
162
+ 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))]"
163
+ >
164
+ Configuration guide
165
+ <ExternalLink className="h-3 w-3" aria-hidden />
166
+ </a>
167
+ <a
168
+ href={ASSISTANT_DOCS_URL}
169
+ target="_blank"
170
+ rel="noreferrer"
171
+ 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))]"
172
+ >
173
+ Learn more
174
+ <ExternalLink className="h-3 w-3" aria-hidden />
175
+ </a>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+
182
+ const AssistantNeedsConfigFile = () => (
183
+ <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">
184
+ <div className="flex items-start gap-3 px-4 py-3.5">
185
+ <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))]">
186
+ <Wrench className="h-4 w-4" aria-hidden />
187
+ </span>
188
+ <div className="flex-1 min-w-0">
189
+ <p className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">Add an eventcatalog.chat.js file</p>
190
+ <p className="mt-1 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
191
+ The Agent needs an{' '}
192
+ <code className="rounded bg-[rgb(var(--ec-page-bg))] px-1 py-0.5 font-mono text-[11px]">eventcatalog.chat.js</code> file
193
+ in your catalog directory and a model provider (bring your own model) before it can answer questions.
194
+ </p>
195
+ <div className="mt-3 flex flex-wrap items-center gap-3">
196
+ <a
197
+ href={ASSISTANT_CONFIGURATION_DOCS_URL}
198
+ target="_blank"
199
+ rel="noreferrer"
200
+ 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))]"
201
+ >
202
+ Configuration guide
203
+ <ExternalLink className="h-3 w-3" aria-hidden />
204
+ </a>
205
+ <a
206
+ href={ASSISTANT_DOCS_URL}
207
+ target="_blank"
208
+ rel="noreferrer"
209
+ 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))]"
210
+ >
211
+ Learn more
212
+ <ExternalLink className="h-3 w-3" aria-hidden />
213
+ </a>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ );
@@ -0,0 +1,265 @@
1
+ import { ArrowUpRight, Check } from 'lucide-react';
2
+ import { Row, cn } from './Row';
3
+ import { PRICING_URL } from './SettingsShared';
4
+
5
+ export type PlanId = 'community' | 'starter' | 'scale' | 'enterprise';
6
+
7
+ interface Props {
8
+ currentPlan: PlanId;
9
+ }
10
+
11
+ interface Plan {
12
+ id: PlanId;
13
+ name: string;
14
+ tagline: string;
15
+ price: string;
16
+ priceSuffix?: string;
17
+ audience: string;
18
+ features: string[];
19
+ ctaLabel: string;
20
+ ctaHref: string;
21
+ /** Visual accent applied to selected/featured cards. */
22
+ accent?: 'default' | 'highlight';
23
+ }
24
+
25
+ const PLANS: Plan[] = [
26
+ {
27
+ id: 'community',
28
+ name: 'Community',
29
+ tagline: 'Open source. Self-hosted.',
30
+ price: 'Free',
31
+ audience: 'For individuals and open source projects.',
32
+ features: [
33
+ 'Document domains, services, events, commands and queries',
34
+ 'Visualize your architecture',
35
+ 'Public schema fetch & sync',
36
+ 'Schema Explorer (Basic)',
37
+ 'Owners, versioning, and flows',
38
+ 'Community support (Discord)',
39
+ ],
40
+ ctaLabel: 'Get started',
41
+ ctaHref: PRICING_URL,
42
+ },
43
+ {
44
+ id: 'starter',
45
+ name: 'Starter',
46
+ tagline: 'For teams customising and scaling.',
47
+ price: '$199',
48
+ priceSuffix: '/month',
49
+ audience: 'Up to 20 active users.',
50
+ features: [
51
+ 'Everything in Community',
52
+ 'Custom landing page',
53
+ 'Bring your own documentation',
54
+ 'Embed Miro, IcePanel, Lucid, DrawIO, FigJam',
55
+ 'EventCatalog Assistant',
56
+ 'Remove EventCatalog branding',
57
+ 'Custom themes',
58
+ 'Email support',
59
+ ],
60
+ ctaLabel: 'Try Starter for 14 days',
61
+ ctaHref: 'https://eventcatalog.cloud',
62
+ },
63
+ {
64
+ id: 'scale',
65
+ name: 'Scale',
66
+ tagline: 'For platform teams managing multiple catalogs.',
67
+ price: '$399',
68
+ priceSuffix: '/month',
69
+ audience: 'Up to 50 active users.',
70
+ accent: 'highlight',
71
+ features: [
72
+ 'Everything in Starter',
73
+ 'Field Intelligence',
74
+ 'Resource-level documentation',
75
+ 'EventCatalog MCP server',
76
+ 'Architecture Change Detection',
77
+ 'Private schema fetch & sync',
78
+ 'GitHub authentication',
79
+ 'Federate up to 3 catalogs',
80
+ 'Custom tools for AI Assistant',
81
+ 'Slack bot integration',
82
+ ],
83
+ ctaLabel: 'Try Scale for 14 days',
84
+ ctaHref: 'https://eventcatalog.cloud',
85
+ },
86
+ {
87
+ id: 'enterprise',
88
+ name: 'Enterprise',
89
+ tagline: 'For complex systems and regulated environments.',
90
+ price: 'Contact us',
91
+ audience: 'Unlimited users, all integrations included.',
92
+ features: [
93
+ 'Everything in Scale',
94
+ 'Unlimited users',
95
+ 'Unlimited integrations',
96
+ 'SSO & SAML',
97
+ 'Advanced governance & audit logging',
98
+ 'Migration & onboarding support',
99
+ 'Dedicated account manager',
100
+ ],
101
+ ctaLabel: 'Schedule a call',
102
+ ctaHref: 'mailto:hello@eventcatalog.dev?subject=Enterprise%20Plan%20Enquiry',
103
+ },
104
+ ];
105
+
106
+ const PLAN_LABELS: Record<PlanId, string> = {
107
+ community: 'Community',
108
+ starter: 'Starter',
109
+ scale: 'Scale',
110
+ enterprise: 'Enterprise',
111
+ };
112
+
113
+ const PLAN_RANK: Record<PlanId, number> = {
114
+ community: 0,
115
+ starter: 1,
116
+ scale: 2,
117
+ enterprise: 3,
118
+ };
119
+
120
+ export const BillingSettingsForm = ({ currentPlan }: Props) => {
121
+ const isPaid = currentPlan === 'starter' || currentPlan === 'scale' || currentPlan === 'enterprise';
122
+ const upgradable = currentPlan !== 'enterprise';
123
+ // Show the current plan and any tier above it. Hides plans the user has already passed.
124
+ const visiblePlans = PLANS.filter((plan) => PLAN_RANK[plan.id] >= PLAN_RANK[currentPlan]);
125
+ const gridCols =
126
+ visiblePlans.length === 1 ? 'grid-cols-1' : visiblePlans.length === 2 ? 'lg:grid-cols-2' : 'lg:grid-cols-2 xl:grid-cols-3';
127
+
128
+ return (
129
+ <div className="divide-y divide-[rgb(var(--ec-page-border))]">
130
+ <Row
131
+ title="Billing plan"
132
+ description="View and manage your billing plan. Upgrade unlocks the AI assistant, MCP server, and other Scale-only features."
133
+ canEdit={false}
134
+ dirty={false}
135
+ >
136
+ <div className="flex flex-wrap 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">
137
+ <div className="flex items-center gap-2 text-[13px] text-[rgb(var(--ec-page-text))]">
138
+ <span className="text-[rgb(var(--ec-page-text-muted))]">Current plan:</span>
139
+ <span className="font-semibold">{PLAN_LABELS[currentPlan]}</span>
140
+ {isPaid && (
141
+ <span className="rounded-full border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-500">
142
+ Active
143
+ </span>
144
+ )}
145
+ </div>
146
+ <div className="ml-auto">
147
+ {upgradable ? (
148
+ <a
149
+ href={PRICING_URL}
150
+ target="_blank"
151
+ rel="noreferrer"
152
+ className="inline-flex items-center gap-1 rounded-md bg-[rgb(var(--ec-page-text))] px-3 py-1.5 text-[12px] font-semibold text-[rgb(var(--ec-page-bg))] transition-opacity hover:opacity-90"
153
+ >
154
+ Upgrade
155
+ <ArrowUpRight className="h-3 w-3" aria-hidden />
156
+ </a>
157
+ ) : (
158
+ <a
159
+ href={PRICING_URL}
160
+ target="_blank"
161
+ rel="noreferrer"
162
+ className="inline-flex items-center gap-1 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 py-1.5 text-[12px] font-medium text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-page-bg)/0.78)]"
163
+ >
164
+ Manage billing
165
+ <ArrowUpRight className="h-3 w-3" aria-hidden />
166
+ </a>
167
+ )}
168
+ </div>
169
+ </div>
170
+ </Row>
171
+
172
+ <Row
173
+ title={upgradable ? 'Upgrade' : 'Your plan'}
174
+ description={
175
+ upgradable
176
+ ? 'Plans available to upgrade to. Pricing shown in USD; visit our website for EUR pricing and the full feature comparison.'
177
+ : 'You are on our highest tier. Visit the website for the full feature comparison.'
178
+ }
179
+ canEdit={false}
180
+ dirty={false}
181
+ >
182
+ <div className={cn('grid grid-cols-1 gap-3', gridCols)}>
183
+ {visiblePlans.map((plan) => (
184
+ <PlanCard key={plan.id} plan={plan} isCurrent={plan.id === currentPlan} />
185
+ ))}
186
+ </div>
187
+ <p className="mt-4 text-[12px] text-[rgb(var(--ec-page-text-muted))]">
188
+ Prices update with annual billing.{' '}
189
+ <a href={PRICING_URL} target="_blank" rel="noreferrer" className="text-[rgb(var(--ec-accent))] hover:underline">
190
+ See full plan comparison →
191
+ </a>
192
+ </p>
193
+ </Row>
194
+ </div>
195
+ );
196
+ };
197
+
198
+ interface PlanCardProps {
199
+ plan: Plan;
200
+ isCurrent: boolean;
201
+ }
202
+
203
+ const PlanCard = ({ plan, isCurrent }: PlanCardProps) => (
204
+ <div
205
+ className={cn(
206
+ 'flex flex-col rounded-xl border p-5 transition-colors',
207
+ isCurrent
208
+ ? 'border-[rgb(var(--ec-accent)/0.5)] bg-[rgb(var(--ec-accent-subtle)/0.4)]'
209
+ : plan.accent === 'highlight'
210
+ ? 'border-[rgb(var(--ec-page-text)/0.25)] bg-[rgb(var(--ec-input-bg,var(--ec-page-bg)))]'
211
+ : 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg,var(--ec-page-bg)))]'
212
+ )}
213
+ >
214
+ <div className="flex items-start justify-between gap-2">
215
+ <div>
216
+ <p className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">{plan.name}</p>
217
+ <p className="mt-0.5 text-[12px] leading-snug text-[rgb(var(--ec-page-text-muted))]">{plan.tagline}</p>
218
+ </div>
219
+ {isCurrent && (
220
+ <span className="rounded-full border border-[rgb(var(--ec-accent)/0.4)] bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-accent-text))]">
221
+ Current
222
+ </span>
223
+ )}
224
+ {!isCurrent && plan.accent === 'highlight' && (
225
+ <span className="rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-500">
226
+ Popular
227
+ </span>
228
+ )}
229
+ </div>
230
+
231
+ <div className="mt-4">
232
+ <span className="text-[24px] font-semibold leading-none tracking-tight text-[rgb(var(--ec-page-text))]">{plan.price}</span>
233
+ {plan.priceSuffix && <span className="ml-1 text-[12px] text-[rgb(var(--ec-page-text-muted))]">{plan.priceSuffix}</span>}
234
+ <p className="mt-1 text-[11px] text-[rgb(var(--ec-page-text-muted))]">{plan.audience}</p>
235
+ </div>
236
+
237
+ <a
238
+ href={plan.ctaHref}
239
+ target="_blank"
240
+ rel="noreferrer"
241
+ aria-disabled={isCurrent}
242
+ onClick={isCurrent ? (e) => e.preventDefault() : undefined}
243
+ className={cn(
244
+ 'mt-4 inline-flex items-center justify-center gap-1 rounded-md px-3 py-1.5 text-[12px] font-semibold transition-colors',
245
+ isCurrent
246
+ ? 'pointer-events-none cursor-default border border-[rgb(var(--ec-page-border))] bg-transparent text-[rgb(var(--ec-page-text-muted))]'
247
+ : plan.accent === 'highlight'
248
+ ? 'bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] hover:opacity-90'
249
+ : 'border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-page-bg)/0.78)]'
250
+ )}
251
+ >
252
+ {isCurrent ? 'Current plan' : plan.ctaLabel}
253
+ {!isCurrent && <ArrowUpRight className="h-3 w-3" aria-hidden />}
254
+ </a>
255
+
256
+ <ul className="mt-5 space-y-1.5">
257
+ {plan.features.map((feature) => (
258
+ <li key={feature} className="flex items-start gap-2 text-[12px] leading-snug text-[rgb(var(--ec-page-text))]">
259
+ <Check className="mt-0.5 h-3 w-3 flex-shrink-0 text-[rgb(var(--ec-page-text-muted))]" aria-hidden />
260
+ <span>{feature}</span>
261
+ </li>
262
+ ))}
263
+ </ul>
264
+ </div>
265
+ );