Skip to content

Commit 012e2af

Browse files
bloveclaude
andauthored
feat(website): Basecamp-informed homepage — Yes wall, proof strip, problem-first hero (#885)
* docs(website): design spec for Basecamp-informed homepage changes Three changes drawn from basecamp.com: a "Yes wall" replacing the Differentiator grid, a specific-proof strip, and a problem-first hero subheadline. Visual direction is Inverted x Specimen — one dark section using the dark tokens the site already ships but never consumes. All four proof-strip figures were verified against live sources and all four were wrong or misleading, so change D is marked blocked pending a rebuild. Verification results and the reasoning are recorded in the spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): update homepage spec for all-MIT (#881) and Angular 22 plans Licensing becomes an unqualified Yes-wall row, the CopilotKit FAQ item is gone from main and stays gone, telemetry row adopts the stronger 'installation is inert' phrasing, and the cut Angular LTS proof cell is conditionally revivable once ^22 peer support lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): approve four surface treatments for homepage spec Marker highlight, dark-band glow/gradient atmosphere, editorial left-rail asymmetry, and paper elevation — all four approved, with exact CSS values from the rendered mockups. Treatment C moves the SectionHeader extraction from out-of-scope to in-scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): implementation plan for Basecamp-informed homepage Nine tasks: dark section substrate via CSS-variable re-scoping, SectionHeader primitive, hero rewrite, YesWall replacing Differentiator, ProofStrip with live HVTrust badge, opt-in dark FinalCTA, FAQ shrink, and a verification gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(website): dark section surface with scoped token re-map * fix(website): re-scope accent-border-hover in dark section * feat(website): SectionHeader primitive with centered and rail variants Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(website): problem-first hero subhead with capability chips and marker highlight * test(website): pin hero copy to HERO_SUBHEAD and scope chip assertions * feat(website): YesWall component — 16 questions, 4 groups, dark specimen * feat(website): replace Differentiator with the Yes wall * test(website): point e2e landing assertion at the Yes wall * feat(website): proof strip — three verified cells with live HVTrust badge * fix(website): reserve badge aspect ratio and trim referrer * feat(website): dark FinalCTA variant, homepage-only * fix(website): clip dark FinalCTA bleed, correct spec mock, size mark for mobile * refactor(website): shrink HomeFAQ to non-binary questions * fix(website): true up Yes wall aside, analytics track, and glow containment --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 2e6197f commit 012e2af

24 files changed

Lines changed: 2677 additions & 370 deletions

apps/website/e2e/website.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ test('landing page renders hero headline', async ({ page }) => {
77
expect(headline?.toLowerCase()).toContain('angular');
88
});
99

10-
test('landing page renders differentiator section', async ({ page }) => {
10+
test('landing page renders the Yes wall', async ({ page }) => {
1111
await page.goto('/');
12-
// Assert on the stable id, not the heading copy — landing-page text rewrites
13-
// happen far more often than the section structure changes.
14-
await expect(page.locator('#differentiator-heading')).toBeVisible();
12+
await expect(page.locator('#yes-wall-heading')).toBeVisible();
1513
});
1614

1715
test('landing page renders feature blocks (Stream/Render/Ship)', async ({ page }) => {

apps/website/src/app/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Hero } from '../components/landing/Hero';
22
import { EcosystemStrip } from '../components/landing/EcosystemStrip';
3-
import { Differentiator } from '../components/landing/Differentiator';
3+
import { YesWall } from '../components/landing/YesWall';
44
import { FeatureBlock } from '../components/landing/FeatureBlock';
55
import { BrowserFrame } from '../components/ui/BrowserFrame';
66
import { ClipPlayer } from '../components/ui/ClipPlayer';
@@ -11,6 +11,7 @@ import { HighlightedCode } from '../components/landing/HighlightedCode';
1111
import { SECTION_MEDIA } from '../lib/section-media';
1212
import type { SectionMedia } from '../lib/section-media';
1313
import { PilotBlock } from '../components/landing/PilotBlock';
14+
import { ProofStrip } from '../components/landing/ProofStrip';
1415
import { WhitePaperBlock } from '../components/landing/WhitePaperBlock';
1516
import { Promises } from '../components/landing/Promises';
1617
import { HomeFAQ } from '../components/landing/HomeFAQ';
@@ -105,7 +106,7 @@ export default async function HomePage() {
105106
<>
106107
<Hero />
107108
<EcosystemStrip />
108-
<Differentiator />
109+
<YesWall />
109110

110111
{/* Interactive demo showcase */}
111112
<Section surface="canvas">
@@ -183,7 +184,7 @@ export default async function HomePage() {
183184
/>
184185

185186
{/*
186-
The homepage claims human-in-the-loop in the Differentiator table and
187+
The homepage claims human-in-the-loop in the Yes wall and
187188
mentions interrupts in the Stream block, but nothing here showed it.
188189
This is the only section whose heading the approval clip actually
189190
illustrates — the same rule the solutions pages follow.
@@ -220,8 +221,9 @@ export default async function HomePage() {
220221
<PilotBlock />
221222
<WhitePaperBlock />
222223
<Promises />
224+
<ProofStrip />
223225
<HomeFAQ />
224-
<FinalCTA />
226+
<FinalCTA variant="dark" />
225227
<RecentArticles />
226228
</>
227229
);

apps/website/src/components/landing/Differentiator.spec.tsx

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

apps/website/src/components/landing/Differentiator.tsx

Lines changed: 0 additions & 146 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT
2+
// @vitest-environment jsdom
3+
import { render } from '@testing-library/react';
4+
import { describe, it, expect, vi } from 'vitest';
5+
import { FinalCTA } from './FinalCTA';
6+
7+
vi.mock('../../lib/analytics/client', () => ({
8+
track: vi.fn(),
9+
trackCtaClick: vi.fn(),
10+
trackExternalLinkClick: vi.fn(),
11+
}));
12+
13+
describe('FinalCTA', () => {
14+
it('defaults to the tinted surface (used by 8 non-home pages)', () => {
15+
const { container } = render(<FinalCTA />);
16+
expect(
17+
container.querySelector('[data-ui="section"]')?.getAttribute('data-surface'),
18+
).toBe('tinted');
19+
});
20+
21+
it('renders the dark surface when variant="dark"', () => {
22+
const { container } = render(<FinalCTA variant="dark" />);
23+
const section = container.querySelector('[data-ui="section"]');
24+
expect(section?.getAttribute('data-surface')).toBe('dark');
25+
expect(container.querySelector('.final-cta-mark')).toBeTruthy();
26+
expect(container.querySelector('.final-cta-mark')?.getAttribute('aria-hidden')).toBe('true');
27+
});
28+
});

apps/website/src/components/landing/FinalCTA.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ interface FinalCTAProps {
1414
secondary?: { label: string; href: string; external?: boolean } | null;
1515
/** Optional trailing caption. Defaults to licensing and telemetry line. Pass null to hide. */
1616
caption?: string | null;
17+
/**
18+
* 'dark' renders on the dark band — homepage only, pairing with the Yes
19+
* wall so the inverted treatment appears twice (spec: a lone dark band
20+
* reads as arbitrary). All other pages keep the default tinted surface.
21+
*/
22+
variant?: 'default' | 'dark';
1723
}
1824

1925
const DEFAULT_SECONDARY = { label: 'See each feature in action →', href: 'https://cockpit.threadplane.ai', external: true };
@@ -24,11 +30,21 @@ export function FinalCTA({
2430
primary = null,
2531
secondary = DEFAULT_SECONDARY,
2632
caption = 'All packages are MIT · Production support available · App telemetry off by default',
33+
variant = 'default',
2734
}: FinalCTAProps = {}) {
2835
return (
29-
<Section surface="tinted" ariaLabelledBy="final-cta-heading">
36+
<Section
37+
surface={variant === 'dark' ? 'dark' : 'tinted'}
38+
className={variant === 'dark' ? 'final-cta-dark' : undefined}
39+
ariaLabelledBy="final-cta-heading"
40+
>
3041
<Container>
3142
<div className="final-cta-inner">
43+
{variant === 'dark' ? (
44+
<div className="final-cta-mark" aria-hidden="true">
45+
46+
</div>
47+
) : null}
3248
<h2 id="final-cta-heading" className="final-cta-heading">
3349
{headline}
3450
</h2>

apps/website/src/components/landing/Hero.spec.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// @vitest-environment jsdom
33
import React from 'react';
44
import { describe, expect, it, vi, beforeEach } from 'vitest';
5-
import { render, screen, fireEvent } from '@testing-library/react';
6-
import { HERO_SUBHEAD, POSITIONING_PROOF_POINTS } from '../../lib/positioning';
5+
import { render, screen, fireEvent, within } from '@testing-library/react';
6+
import { HERO_CHIPS, HERO_SUBHEAD, POSITIONING_PROOF_POINTS } from '../../lib/positioning';
77

88
const trackMock = vi.hoisted(() => vi.fn());
99
const writeTextMock = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
@@ -46,14 +46,21 @@ beforeEach(() => {
4646
});
4747

4848
describe('Hero', () => {
49-
it('renders the locked H1 and subhead', async () => {
49+
it('renders the locked H1, problem-first subhead, and capability chips', async () => {
5050
const { Hero } = await import('./Hero');
5151
render(<Hero />);
5252
expect(screen.getByRole('heading', { level: 1 }).textContent)
53-
.toMatch(/Ship production agent UIs in Angular\./);
54-
expect(screen.getByText(HERO_SUBHEAD)).toBeTruthy();
53+
.toBe('Ship production agent UIs in Angular.');
54+
const subhead = document.querySelector('.hero-subhead');
55+
expect(subhead?.textContent).toBe(HERO_SUBHEAD);
56+
const chipRow = screen.getByRole('list', { name: 'Capabilities' });
57+
for (const chip of HERO_CHIPS) {
58+
expect(within(chipRow).getByText(chip)).toBeTruthy();
59+
}
5560
for (const proofPoint of POSITIONING_PROOF_POINTS) {
56-
expect(screen.getByText(proofPoint.label)).toBeTruthy();
61+
// 'LangGraph + AG-UI' appears both as a chip and as a proof pill label,
62+
// so use getAllByText rather than getByText (which throws on multiple matches).
63+
expect(screen.getAllByText(proofPoint.label).length).toBeGreaterThanOrEqual(1);
5764
}
5865
});
5966

0 commit comments

Comments
 (0)