+ {/* Three lines. The subhead names every capability, and at 24px/470
+ it ran to four and overlapped the pills — the column is centred in
+ a fixed 630px card, so overflow collides rather than pushing. 530
+ is the widest the 600px column's 64px left padding allows, and 20px
+ is what holds three lines without orphaning the last two words. */}
+
{HERO_SUBHEAD}
diff --git a/apps/website/src/app/page.tsx b/apps/website/src/app/page.tsx
index 5d6c39df1..b78bc52ec 100644
--- a/apps/website/src/app/page.tsx
+++ b/apps/website/src/app/page.tsx
@@ -4,19 +4,16 @@ import { EnterpriseArchitecture } from '../components/landing/EnterpriseArchitec
import { Stage } from '../components/landing/Stage';
import { TeamsBlock } from '../components/landing/TeamsBlock';
import { HomeFAQ } from '../components/landing/HomeFAQ';
-import { FinalCTA } from '../components/landing/FinalCTA';
+import { OpenSourceStrip } from '../components/landing/OpenSourceStrip';
import { RecentArticles } from '../components/landing/RecentArticles';
-import { PROVE_IT_ROWS } from '../lib/positioning';
// The homepage must stay statically rendered (no cookies()/headers()/dynamic):
// the proof lines are read from the demo recording at build time, and the file
// is traced into the deployment only as a safety net.
import { STAGE_PROOF } from '../lib/stage-proof';
import {
createPageMetadata,
- HERO_SECONDARY_HREF,
HOME_DESCRIPTION,
HOME_TITLE,
- INSTALL_OPTIONS,
} from '../lib/site-metadata';
import { getFormPolicy } from '../lib/growth/form-policy';
@@ -41,30 +38,9 @@ export default function HomePage() {
(live-stage spec §3, §8). Copy lives in STAGE_RAIL (positioning.ts). */}
-
+ {/* The open-source beat: a slim dark strip, not a second CTA. Copy lives
+ in OPEN_SOURCE_STRIP (positioning.ts). */}
+
diff --git a/apps/website/src/components/landing/FinalCTA.spec.tsx b/apps/website/src/components/landing/FinalCTA.spec.tsx
index f44ec944c..791d475a8 100644
--- a/apps/website/src/components/landing/FinalCTA.spec.tsx
+++ b/apps/website/src/components/landing/FinalCTA.spec.tsx
@@ -91,29 +91,6 @@ describe('FinalCTA', () => {
expect(screen.getByRole('link', { name: 'Talk to an engineer' }).getAttribute('href')).toBe('/contact');
});
- it('renders optional rows above the headline in the claim/api grammar', () => {
- render(
- ,
- );
- const list = screen.getByRole('list', { name: 'What you can prove first' });
- expect(list.querySelectorAll('li')).toHaveLength(2);
- expect(screen.getByText('provideFakeAgent()')).toBeTruthy();
- const heading = screen.getByRole('heading', { level: 2 });
- expect(list.compareDocumentPosition(heading) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
- expect(list.querySelector('.final-cta-prove-row-claim')?.textContent).toBe('No key, no server, no network');
- expect(list.querySelector('.final-cta-prove-row-api')?.textContent).toBe('provideFakeAgent()');
- });
-
- it('renders no rows list when rows are omitted', () => {
- render();
- expect(screen.queryByRole('list', { name: 'What you can prove first' })).toBeNull();
- });
-
it('renders extra caption links after the first, separated by " · "', () => {
render(
) : null}
- {rows.length > 0 ? (
-
- {rows.map((row) => (
-
- {row.claim}
- {row.api}
-
- ))}
-
- ) : null}
{headline}
diff --git a/apps/website/src/components/landing/OpenSourceStrip.spec.tsx b/apps/website/src/components/landing/OpenSourceStrip.spec.tsx
new file mode 100644
index 000000000..0c8857aba
--- /dev/null
+++ b/apps/website/src/components/landing/OpenSourceStrip.spec.tsx
@@ -0,0 +1,74 @@
+// @vitest-environment jsdom
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { OpenSourceStrip } from './OpenSourceStrip';
+import { GITHUB_REPO_URL, OPEN_SOURCE_STRIP } from '../../lib/positioning';
+
+const trackCtaClickMock = vi.hoisted(() => vi.fn());
+vi.mock('../../lib/analytics/client', () => ({
+ track: vi.fn(),
+ trackCtaClick: trackCtaClickMock,
+ trackExternalLinkClick: vi.fn(),
+}));
+
+beforeEach(() => trackCtaClickMock.mockClear());
+
+describe('OpenSourceStrip', () => {
+ it('renders the whole sentence as the section heading, emphasis included', () => {
+ render();
+ const heading = screen.getByRole('heading', { level: 2 });
+ expect(heading.textContent).toBe(
+ `${OPEN_SOURCE_STRIP.lead} ${OPEN_SOURCE_STRIP.emphasis}`,
+ );
+ expect(heading.querySelector('em')?.textContent).toBe(OPEN_SOURCE_STRIP.emphasis);
+ });
+
+ it('names the section by that heading', () => {
+ const { container } = render();
+ const section = container.querySelector('[data-ui="section"]');
+ expect(section?.getAttribute('aria-labelledby')).toBe('open-source-heading');
+ expect(container.querySelector('#open-source-heading')).toBeTruthy();
+ });
+
+ it('sits on the dark surface at the tight rhythm', () => {
+ const { container } = render();
+ const section = container.querySelector('[data-ui="section"]');
+ expect(section?.getAttribute('data-surface')).toBe('dark');
+ // The strip's own padding override keys off [data-tight]; dropping the
+ // prop silently restores the full 48-80px band this replaced.
+ expect(section?.getAttribute('data-tight')).toBe('true');
+ expect(section?.classList.contains('open-source-strip')).toBe(true);
+ });
+
+ it('links the repo in a new tab, with the mark beside the label', () => {
+ const { container } = render();
+ const link = screen.getByRole('link', { name: OPEN_SOURCE_STRIP.cta });
+ expect(link.getAttribute('href')).toBe(GITHUB_REPO_URL);
+ expect(link.getAttribute('target')).toBe('_blank');
+ expect(link.getAttribute('rel')).toBe('noopener noreferrer');
+ // The mark is decorative: the accessible name above must stay the label
+ // alone, so the svg has to be hidden rather than merely unlabelled.
+ const svg = link.querySelector('svg');
+ expect(svg).toBeTruthy();
+ expect(svg?.getAttribute('aria-hidden')).toBe('true');
+ expect(container.querySelector('.open-source-strip-licence')?.textContent).toBe(
+ OPEN_SOURCE_STRIP.licence,
+ );
+ });
+
+ it('is the page’s quiet beat: one action, no second CTA', () => {
+ const { container } = render();
+ expect(container.querySelectorAll('a').length).toBe(1);
+ });
+
+ it('reports the fork click as a homepage CTA', () => {
+ render();
+ fireEvent.click(screen.getByRole('link', { name: OPEN_SOURCE_STRIP.cta }));
+ expect(trackCtaClickMock).toHaveBeenCalledWith({
+ cta_id: 'hero_github',
+ track: 'developer',
+ surface: 'home',
+ destination_url: GITHUB_REPO_URL,
+ });
+ });
+});
diff --git a/apps/website/src/components/landing/OpenSourceStrip.tsx b/apps/website/src/components/landing/OpenSourceStrip.tsx
new file mode 100644
index 000000000..085195606
--- /dev/null
+++ b/apps/website/src/components/landing/OpenSourceStrip.tsx
@@ -0,0 +1,62 @@
+'use client';
+
+import { Container } from '../ui/Container';
+import { Section } from '../ui/Section';
+import { Button } from '../ui/Button';
+import { GitHubIcon } from '../ui/GitHubIcon';
+import { trackCtaClick } from '../../lib/analytics/client';
+import { GITHUB_REPO_URL, OPEN_SOURCE_STRIP } from '../../lib/positioning';
+
+/**
+ * The homepage's open-source beat: one dark strip between the stage and the
+ * teams block.
+ *
+ * It replaced a full dark `FinalCTA` (the "prove the Angular UI" closer). The
+ * point of the section is that there is no catch and no upsell, so it is
+ * deliberately the quietest band on the page: one sentence at the left edge of
+ * the page container, the licence and the repo at the right, and no second
+ * pitch. The four library pages still close on `FinalCTA variant="dark"` —
+ * that component is untouched.
+ */
+export function OpenSourceStrip() {
+ return (
+
+
+