Skip to content

Commit e8dc1ba

Browse files
bloveclaude
andauthored
feat(website): tighten the pricing page (#908)
* docs(website): design spec for pricing page tightening Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): implementation plan for pricing tightening Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(website): lead form takes center stage — tier restatement cards deleted * feat(website): comparison table keeps only rows that differ * feat(website): compatibility collapses into the hero license strip * copy(website): pricing FAQ keeps only genuinely open questions * refactor(website): pricing rail headers and centered tier grid --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent fc65736 commit e8dc1ba

14 files changed

Lines changed: 603 additions & 418 deletions

apps/website/src/app/pricing/page.spec.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('PricingPage', () => {
1616
expect(screen.getByText(/Every package is MIT/i)).toBeTruthy();
1717
expect(screen.getByText(/commercial products, internal tools, and client work/i)).toBeTruthy();
1818
expect(screen.getByText(/No Threadplane cloud/i)).toBeTruthy();
19+
expect(screen.getByText(/Angular 20, 21, 22, CI-tested/i)).toBeTruthy();
1920
expect(screen.getByRole('heading', { level: 2, name: /Open software/i })).toBeTruthy();
2021
expect(screen.getByText(/does not host your agents or conversations/i)).toBeTruthy();
2122
expect(screen.getByText(/durable persistence.*connected backend/i)).toBeTruthy();
@@ -27,7 +28,9 @@ describe('PricingPage', () => {
2728
expect(screen.getByRole('table', { name: /Full plan comparison/i })).toBeTruthy();
2829
expect(screen.getByRole('columnheader', { name: 'Community' })).toBeTruthy();
2930
expect(screen.getByRole('columnheader', { name: 'Production Assurance' })).toBeTruthy();
30-
expect(screen.getByRole('rowheader', { name: 'MIT-licensed software' })).toBeTruthy();
31+
expect(screen.queryByRole('rowheader', { name: 'MIT-licensed software' })).toBeNull();
32+
expect(screen.getByRole('rowheader', { name: 'Private support channel' })).toBeTruthy();
33+
expect(screen.getByText(/identical on every path/i)).toBeTruthy();
3134
expect(container.querySelectorAll('table[aria-label="Full plan comparison"]')).toHaveLength(1);
3235
});
3336

apps/website/src/app/pricing/page.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { Section } from '../../components/ui/Section';
33
import { Eyebrow } from '../../components/ui/Eyebrow';
44
import { CompareTable } from '../../components/pricing/CompareTable';
55
import { ArchitectureBoundary, PricingComparison } from '../../components/pricing/PricingDetails';
6-
import { CompatibilityMatrix } from '../../components/pricing/CompatibilityMatrix';
76
import { PricingFAQ } from '../../components/pricing/PricingFAQ';
87
import { LeadForm } from '../../components/pricing/LeadForm';
98
import { FinalCTA } from '../../components/landing/FinalCTA';
109
import { createPageMetadata } from '../../lib/site-metadata';
10+
import { WEBSITE_SUPPORTED_ANGULAR_VERSIONS } from '../../components/pricing/angular-support.mjs';
1111

1212
export const metadata = createPageMetadata({
1313
title: 'Pricing — Threadplane',
@@ -37,6 +37,8 @@ export default function PricingPage() {
3737
<span aria-hidden="true">·</span>
3838
<span>Commercial use without registration or runtime checks</span>
3939
<span aria-hidden="true">·</span>
40+
<span>{WEBSITE_SUPPORTED_ANGULAR_VERSIONS}, CI-tested</span>
41+
<span aria-hidden="true">·</span>
4042
<strong>No Threadplane cloud</strong>
4143
</p>
4244
</div>
@@ -57,19 +59,6 @@ export default function PricingPage() {
5759
</Container>
5860
</Section>
5961

60-
<Section surface="canvas">
61-
<Container>
62-
<Eyebrow className="pricing-page-eyebrow-tight">Compatibility</Eyebrow>
63-
<h2 className="pricing-page-h2">
64-
Angular version support
65-
</h2>
66-
<p className="pricing-page-compat-body">
67-
We ship against the versions our CI tests. Other versions may work but aren&apos;t guaranteed.
68-
</p>
69-
<CompatibilityMatrix />
70-
</Container>
71-
</Section>
72-
7362
<PricingFAQ />
7463

7564
<LeadForm />

apps/website/src/components/pricing/CompatibilityMatrix.spec.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

apps/website/src/components/pricing/CompatibilityMatrix.tsx

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2+
import { describe, it, expect, vi, beforeEach } from 'vitest';
3+
import { LeadForm } from './LeadForm';
4+
5+
const trackMock = vi.fn();
6+
vi.mock('../../lib/analytics/client', async (importOriginal) => {
7+
const mod = await importOriginal<Record<string, unknown>>();
8+
return { ...mod, track: (...args: unknown[]) => trackMock(...args) };
9+
});
10+
11+
describe('LeadForm', () => {
12+
beforeEach(() => {
13+
trackMock.mockClear();
14+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
15+
});
16+
17+
it('submits and fires the lead analytics with surface/source_section, then shows the sent state', async () => {
18+
render(<LeadForm />);
19+
fireEvent.change(screen.getByLabelText(/^name$/i), { target: { value: 'Dev' } });
20+
fireEvent.change(screen.getByLabelText(/work email/i), { target: { value: 'dev@example.com' } });
21+
fireEvent.change(screen.getByLabelText(/^company$/i), { target: { value: 'Acme' } });
22+
fireEvent.submit(screen.getByRole('button', { name: /request enterprise quote/i }).closest('form')!);
23+
24+
await waitFor(() => {
25+
expect(screen.getByText(/we'll be in touch within one business day/i)).toBeTruthy();
26+
});
27+
28+
expect(trackMock).toHaveBeenCalledWith('marketing:lead_form_submit', {
29+
surface: 'pricing',
30+
source_section: 'lead-form',
31+
});
32+
expect(trackMock).toHaveBeenCalledWith('marketing:lead_form_success', {
33+
surface: 'pricing',
34+
source_section: 'lead-form',
35+
});
36+
expect(trackMock).not.toHaveBeenCalledWith('marketing:lead_form_fail', expect.anything());
37+
});
38+
39+
it('fires the fail event with error_reason api_error when the request fails', async () => {
40+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false }));
41+
render(<LeadForm />);
42+
fireEvent.change(screen.getByLabelText(/^name$/i), { target: { value: 'Dev' } });
43+
fireEvent.change(screen.getByLabelText(/work email/i), { target: { value: 'dev@example.com' } });
44+
fireEvent.change(screen.getByLabelText(/^company$/i), { target: { value: 'Acme' } });
45+
fireEvent.submit(screen.getByRole('button', { name: /request enterprise quote/i }).closest('form')!);
46+
47+
await waitFor(() => {
48+
expect(screen.getByText(/something went wrong/i)).toBeTruthy();
49+
});
50+
51+
expect(trackMock).toHaveBeenCalledWith('marketing:lead_form_fail', {
52+
surface: 'pricing',
53+
source_section: 'lead-form',
54+
error_reason: 'api_error',
55+
});
56+
});
57+
58+
it('renders the See how Pilot-to-Prod works link pointing at /pilot-to-prod', () => {
59+
render(<LeadForm />);
60+
const link = screen.getByRole('link', { name: /see how pilot-to-prod works/i });
61+
expect(link.getAttribute('href')).toBe('/pilot-to-prod');
62+
});
63+
});

apps/website/src/components/pricing/LeadForm.tsx

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,6 @@ import { Eyebrow } from '../ui/Eyebrow';
88
import { Button } from '../ui/Button';
99
import { Card } from '../ui/Card';
1010

11-
const VALUE_PROPS = [
12-
{
13-
title: 'Production assurance',
14-
body: 'Architecture guidance, private support, and response commitments scoped to your production needs.',
15-
},
16-
{
17-
title: 'SLA + security review',
18-
body: 'Response SLAs, security questionnaires, and a private support channel.',
19-
},
20-
{
21-
title: 'Optional Pilot-to-Prod engagement',
22-
body: 'A separately scoped delivery engagement for teams that want guided implementation from prototype through production.',
23-
highlight: true,
24-
link: { href: '/pilot-to-prod', label: 'See how Pilot-to-Prod works →' },
25-
},
26-
{
27-
title: 'Procurement support',
28-
body: 'Master services agreement where applicable, security review, and custom indemnification where contractually agreed.',
29-
},
30-
];
31-
3211
export function LeadForm() {
3312
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
3413
const [pilotInterest, setPilotInterest] = useState<'yes' | 'maybe' | 'no'>('maybe');
@@ -77,45 +56,22 @@ export function LeadForm() {
7756
<Container>
7857
<div className="lead-form-wrap">
7958
<div className="lead-form-header">
80-
<Eyebrow tone="accent" className="lead-form-eyebrow">Enterprise</Eyebrow>
59+
<div className="lead-form-rail">
60+
<Eyebrow tone="accent" className="lead-form-eyebrow">Enterprise</Eyebrow>
61+
<span className="lead-form-rail-line" aria-hidden="true" />
62+
</div>
8163
<h2 id="lead-form-heading" className="lead-form-heading">
8264
Choose the support.<br />Add delivery if you need it.
8365
</h2>
8466
<p className="lead-form-subhead">
8567
Production Assurance and Pilot-to-Prod are separate choices. Request ongoing support or ask us to scope a hands-on delivery engagement.
8668
</p>
69+
<a href="/pilot-to-prod" className="lead-form-p2p-link">
70+
See how Pilot-to-Prod works →
71+
</a>
8772
</div>
8873

8974
<div className="lead-form-grid">
90-
{/* Value props column */}
91-
<ul className="lead-form-value-list">
92-
{VALUE_PROPS.map((vp) => (
93-
<li
94-
key={vp.title}
95-
className="lead-form-value-item"
96-
data-highlight={vp.highlight || undefined}
97-
>
98-
<span aria-hidden="true" className="lead-form-value-check">
99-
100-
</span>
101-
<div>
102-
<div className="lead-form-value-title">
103-
{vp.title}
104-
</div>
105-
<p className="lead-form-value-body">
106-
{vp.body}
107-
</p>
108-
{vp.link && (
109-
<a href={vp.link.href} className="lead-form-value-link">
110-
{vp.link.label}
111-
</a>
112-
)}
113-
</div>
114-
</li>
115-
))}
116-
</ul>
117-
118-
{/* Form column */}
11975
{status === 'sent' ? (
12076
<Card padding="lg">
12177
<p className="lead-form-sent-message">

0 commit comments

Comments
 (0)