Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/website/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export default function HomePage() {
(live-stage spec §3, §8). Copy lives in STAGE_RAIL (positioning.ts). */}
<Stage proof={STAGE_PROOF} />

{/* The open-source beat: a slim dark strip, not a second CTA. Copy lives
in OPEN_SOURCE_STRIP (positioning.ts). */}
{/* The open-source full stop: a loud dark band with one fork CTA. Copy
lives in OPEN_SOURCE_STRIP (positioning.ts). */}
<OpenSourceStrip />
<TeamsBlock formPolicy={formPolicy} />
<HomeFAQ />
Expand Down
37 changes: 27 additions & 10 deletions apps/website/src/components/landing/OpenSourceStrip.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ vi.mock('../../lib/analytics/client', () => ({
beforeEach(() => trackCtaClickMock.mockClear());

describe('OpenSourceStrip', () => {
it('renders the whole sentence as the section heading, emphasis included', () => {
render(<OpenSourceStrip />);
it('makes the headline alone the section heading', () => {
const { container } = render(<OpenSourceStrip />);
const heading = screen.getByRole('heading', { level: 2 });
expect(heading.textContent).toBe(
`${OPEN_SOURCE_STRIP.lead} ${OPEN_SOURCE_STRIP.emphasis}`,
// Two words, not a sentence: the section's accessible name is the offer.
expect(heading.textContent).toBe(OPEN_SOURCE_STRIP.headline);
expect(heading.querySelector('em')).toBeNull();
// The eyebrow stays readable rather than aria-hidden, matching how
// SectionHeader treats its own eyebrows.
expect(container.querySelector('.open-source-strip-eyebrow')?.textContent).toBe(
OPEN_SOURCE_STRIP.eyebrow,
);
expect(heading.querySelector('em')?.textContent).toBe(OPEN_SOURCE_STRIP.emphasis);
});

it('names the section by that heading', () => {
Expand All @@ -30,13 +34,15 @@ describe('OpenSourceStrip', () => {
expect(container.querySelector('#open-source-heading')).toBeTruthy();
});

it('sits on the dark surface at the tight rhythm', () => {
it('sits on the dark surface at the FULL rhythm, not the tight one', () => {
const { container } = render(<OpenSourceStrip />);
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');
// Deliberately absent. This band used to be the page's quiet beat and a
// guard here asserted data-tight="true" to keep it that way; the section
// is now a full stop, and it gets its ~461px from the standard section
// padding rather than from any override of its own.
expect(section?.getAttribute('data-tight')).toBeNull();
expect(section?.classList.contains('open-source-strip')).toBe(true);
});

Expand All @@ -56,7 +62,7 @@ describe('OpenSourceStrip', () => {
);
});

it('is the page’s quiet beat: one action, no second CTA', () => {
it('offers one action and no second CTA', () => {
const { container } = render(<OpenSourceStrip />);
expect(container.querySelectorAll('a').length).toBe(1);
});
Expand All @@ -71,4 +77,15 @@ describe('OpenSourceStrip', () => {
destination_url: GITHUB_REPO_URL,
});
});

it('marks the runway decorative and puts it outside the container', () => {
const { container } = render(<OpenSourceStrip />);
const runway = container.querySelector('.open-source-strip-runway');
expect(runway?.getAttribute('aria-hidden')).toBe('true');
expect(runway?.textContent).toBe('');
// It spans the section, not the container, so it must not be nested in
// one — inside, the container's gutters would clip the marking short.
expect(runway?.closest('[data-ui="container"]')).toBeNull();
expect(runway?.parentElement?.getAttribute('data-ui')).toBe('section');
});
});
79 changes: 42 additions & 37 deletions apps/website/src/components/landing/OpenSourceStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,60 @@ 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.
* The homepage's open-source beat: a dark full-stop band 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.
* It is deliberately loud. An earlier version was the quietest band on the
* page — the reasoning was that the offer is "no catch, no upsell," so it
* should not read as a second pitch. That restraint is now spent on purpose:
* the open-source offer is one of the strongest things the product has to
* say, and it is said here in two words over an aviation code.
*
* The band takes its ~461px from the standard section rhythm; there is no
* padding override, which is why `tight` is absent rather than false.
*
* The four library pages still close on `FinalCTA variant="dark"` — that
* component is untouched.
*/
export function OpenSourceStrip() {
return (
<Section
surface="dark"
tight
ariaLabelledBy="open-source-heading"
className="open-source-strip"
>
<Container>
<div className="open-source-strip-inner">
<h2 id="open-source-heading" className="open-source-strip-line">
{OPEN_SOURCE_STRIP.lead}{' '}
<em>{OPEN_SOURCE_STRIP.emphasis}</em>
</h2>
<div className="open-source-strip-actions">
<span className="open-source-strip-licence">
{OPEN_SOURCE_STRIP.licence}
</span>
<Button
variant="secondary"
size="md"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
leadingIcon={<GitHubIcon />}
onClick={() =>
trackCtaClick({
cta_id: 'hero_github',
track: 'developer',
surface: 'home',
destination_url: GITHUB_REPO_URL,
})
}
>
{OPEN_SOURCE_STRIP.cta}
</Button>
</div>
<p className="open-source-strip-eyebrow">{OPEN_SOURCE_STRIP.eyebrow}</p>
<h2 id="open-source-heading" className="open-source-strip-headline">
{OPEN_SOURCE_STRIP.headline}
</h2>
<div className="open-source-strip-actions">
<Button
variant="primary"
size="md"
href={GITHUB_REPO_URL}
target="_blank"
rel="noopener noreferrer"
leadingIcon={<GitHubIcon />}
onClick={() =>
trackCtaClick({
cta_id: 'hero_github',
track: 'developer',
surface: 'home',
destination_url: GITHUB_REPO_URL,
})
}
>
{OPEN_SOURCE_STRIP.cta}
</Button>
<span className="open-source-strip-licence">
{OPEN_SOURCE_STRIP.licence}
</span>
</div>
</Container>
{/* Spans the section rather than the container, so the marking runs
edge to edge like paint on a runway. */}
<div className="open-source-strip-runway" aria-hidden="true" />
</Section>
);
}
16 changes: 9 additions & 7 deletions apps/website/src/lib/positioning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,16 @@ describe('positioning: coding-agent prompt', () => {
});

describe('homepage restructure copy (live-stage spec §3)', () => {
it('closes on the open-source offer: one sentence, the licence, the fork CTA', async () => {
it('closes on the open-source offer: an aviation eyebrow, two words, the licence and the CTA', async () => {
const { OPEN_SOURCE_STRIP } = await import('./positioning');
expect(`${OPEN_SOURCE_STRIP.lead} ${OPEN_SOURCE_STRIP.emphasis}`).toBe(
'Yes, this is all free. Don’t like something? Fork us.',
);
// The strip is one line on a wide viewport; long copy would wrap into the
// action group and undo the whole point of the shape.
expect(`${OPEN_SOURCE_STRIP.lead} ${OPEN_SOURCE_STRIP.emphasis}`.length).toBeLessThanOrEqual(60);
expect(OPEN_SOURCE_STRIP.eyebrow).toBe('Squawk 1200');
expect(OPEN_SOURCE_STRIP.headline).toBe('Fork us.');
// A length budget, not a word count: 12 characters is about what fits
// on one line at 116px in Archivo Black inside the 1200px container.
// A wrapped headline stops reading as a full stop.
expect(OPEN_SOURCE_STRIP.headline.length).toBeLessThanOrEqual(12);
// The eyebrow sits on one line at 0.18em tracking beside nothing else.
expect(OPEN_SOURCE_STRIP.eyebrow.length).toBeLessThanOrEqual(14);
expect(OPEN_SOURCE_STRIP.licence).toBe('MIT');
expect(OPEN_SOURCE_STRIP.cta).toBe('Fork on GitHub');
});
Expand Down
24 changes: 15 additions & 9 deletions apps/website/src/lib/positioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,22 @@ export const HERO_TRUST_LINE = `MIT · ${formatAngularRange(WEBSITE_SUPPORTED_AN
*/
export const GITHUB_REPO_URL = 'https://github.com/cacheplane/angular-agent-framework';

// ── The open-source strip (the slim dark band after the stage). One sentence,
// the licence, the repo. Bold in what it says, quiet in how it looks: the
// point is that there is no catch, and the remedy for disagreeing with us is
// a fork — so it must not read as another sales pitch. ──────────────────────
// ── The open-source band (the dark full stop after the stage). An eyebrow,
// two words, the licence and the repo. It used to be the quietest band on the
// page; it is now loud on purpose, because the offer — no catch, and the
// remedy for disagreeing with us is a fork — is one of the strongest things
// the product has to say. ───────────────────────────────────────────────────
export const OPEN_SOURCE_STRIP = {
/** The sentence, up to the part that carries the emphasis. */
lead: 'Yes, this is all free. Don’t like something?',
/** Rendered italic at the end of the same line. */
emphasis: 'Fork us.',
/** The licence tag beside the action. */
/**
* The US transponder code for VFR flight not receiving ATC services —
* flying with nobody controlling you, which is the offer exactly. It is
* texture, not information: the headline carries the whole meaning, so a
* reader who does not fly loses nothing.
*/
eyebrow: 'Squawk 1200',
/** Two words at up to 116px. The band's entire argument. */
headline: 'Fork us.',
/** The bare licence, set in mono after the action. */
licence: 'MIT',
cta: 'Fork on GitHub',
} as const;
Expand Down
95 changes: 57 additions & 38 deletions apps/website/src/styles/landing.css
Original file line number Diff line number Diff line change
Expand Up @@ -616,58 +616,77 @@
}
}

/* OpenSourceStrip — components/landing/OpenSourceStrip.tsx
*
* A strip, not a section: the sentence sits at the left edge of the page
* container and the action at the right, so it reads as an aside between the
* stage and the teams block. It deliberately undercuts the tight section
* rhythm (48–80px) — at that height the band still reads as a full closing
* beat, which is the volume this replaced. The [data-tight] selector is two
* attributes, so the override has to carry both to win. */
[data-ui="section"][data-tight].open-source-strip {
padding-top: clamp(28px, 3.2vw, 44px);
padding-bottom: clamp(28px, 3.2vw, 44px);
}
.open-source-strip-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px 32px;
flex-wrap: wrap;
/* The Fork us band — components/landing/OpenSourceStrip.tsx.
* No padding override: the band takes the standard --spacing-section-y, which
* is where its ~461px comes from. The dark surface already sets position:
* relative, but the runway depends on it, so it is set here too rather than
* inherited from a rule that could change for unrelated reasons. */
.open-source-strip {
position: relative;
}
.open-source-strip-line {
/* Body-scale editorial copy, not display type: it carries an italic <em>,
and Archivo Black has no italic. --font-sans (Archivo) does. */
font-family: var(--font-sans);
font-size: 23px;
line-height: 1.35;
.open-source-strip-eyebrow {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
/* The Eyebrow primitive pins 1.4; this rule is hand-rolled for the tighter
11px/.18em the band wants, so it has to carry the line-height too rather
than inherit the body's. */
line-height: 1.4;
color: var(--color-signal);
margin: 0 0 22px;
}
.open-source-strip-headline {
/* --font-display is available here now: the italic <em> this line used to
carry is gone, and Archivo Black has no italic. */
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.005em;
font-size: clamp(56px, 8.5vw, 116px);
line-height: 0.9;
letter-spacing: -0.04em;
color: var(--color-text-primary);
margin: 0;
}
.open-source-strip-line em {
font-style: italic;
}
.open-source-strip-actions {
display: flex;
align-items: center;
gap: 18px;
flex: none;
gap: 20px;
margin-top: 38px;
}
.open-source-strip-licence {
font-family: var(--font-sans);
font-size: 12px;
letter-spacing: 0.02em;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-text-muted);
border: 1px solid var(--color-border);
border-radius: var(--radius-pill, 999px);
padding: 4px 10px;
white-space: nowrap;
}
/* The runway centreline. A fixed 46px dash on a 92px period rather than a
* proportional one, so it reads as the same painted marking at every width. */
.open-source-strip-runway {
/* Decorative and layered over the band's bottom padding, so it must not
swallow a text-selection drag — the dark surface's top seam
(ui.css, [data-surface="dark"]::before) sets this for the same reason. */
pointer-events: none;
position: absolute;
left: 0;
right: 0;
bottom: 30px;
height: 6px;
background: repeating-linear-gradient(
90deg,
rgba(255, 175, 0, 0.5) 0 46px,
transparent 46px 92px
);
}
@media (max-width: 640px) {
.open-source-strip-line {
font-size: 21px;
.open-source-strip-headline {
font-size: clamp(44px, 13vw, 56px);
}
.open-source-strip-actions {
flex-wrap: wrap;
gap: 14px;
}
}

Expand Down
Loading
Loading