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
12 changes: 8 additions & 4 deletions apps/website/src/components/landing/Hero.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
HERO_H1,
HERO_SECONDARY_HREF,
HERO_SUBHEAD,
HERO_SUBHEAD_SEGMENTS,
} from '../../lib/positioning';

const trackMock = vi.hoisted(() => vi.fn());
Expand Down Expand Up @@ -57,10 +58,13 @@ describe('Hero', () => {
expect(screen.getByRole('heading', { level: 1 }).textContent).toBe(HERO_H1);
expect(screen.getByText(HERO_EYEBROW)).toBeTruthy();
expect(document.querySelector('.hero-subhead')?.textContent).toBe(HERO_SUBHEAD);
expect(document.querySelector('.hero-subhead .marker-highlight')?.textContent).toBe(
'Your backend stays where it is.',
);
expect(document.querySelectorAll('.hero-subhead .marker-highlight')).toHaveLength(1);
expect(document.querySelectorAll('.hero-subhead a.marker-highlight')).toHaveLength(5);
for (const segment of HERO_SUBHEAD_SEGMENTS.filter((s) => s.href)) {
const link = screen.getByRole('link', { name: segment.text, exact: true });
expect(link.getAttribute('href')).toBe(segment.href);
expect(link.getAttribute('target')).toBe('_blank');
expect(link.getAttribute('rel')).toBe('noopener noreferrer');
}
expect(document.querySelector('.hero-chip-row')).toBeNull();
expect(screen.queryByText(/six months/)).toBeNull();
expect(screen.queryByRole('link', { name: /Talk to our engineers/ })).toBeNull();
Expand Down
8 changes: 4 additions & 4 deletions apps/website/src/components/landing/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ export function Hero() {
))}
</h1>
<p className="hero-subhead">
{HERO_SUBHEAD_SEGMENTS.map((segment) =>
segment.highlight ? (
<span className="marker-highlight" key={segment.text}>{segment.text}</span>
{HERO_SUBHEAD_SEGMENTS.map((segment, index) =>
segment.href ? (
<a className="marker-highlight" href={segment.href} target="_blank" rel="noopener noreferrer" key={index}>{segment.text}</a>
) : (
<React.Fragment key={segment.text}>{segment.text}</React.Fragment>
<React.Fragment key={index}>{segment.text}</React.Fragment>
),
)}
</p>
Expand Down
18 changes: 9 additions & 9 deletions apps/website/src/lib/positioning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('positioning: hero copy', () => {
expect(HERO_EYEBROW).toBe('Angular · LangGraph & AG-UI');
expect(HERO_H1).toBe('The open-source thread-plane for agents.');
expect(HERO_SUBHEAD).toBe(
'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.',
'Make agent work persistent, durable, visible, reviewable, and resumable.',
);
expect(HOME_TITLE).toBe('Threadplane — The open-source thread-plane for agents');
expect(HOME_DESCRIPTION).toBe(
Expand All @@ -64,15 +64,15 @@ describe('positioning: hero copy', () => {
expect(HERO_H1_LINES.join(' ')).toBe(HERO_H1);
});

it('subhead segments join back to HERO_SUBHEAD, with exactly one highlight', () => {
// The segments exist only so Hero.tsx can marker-highlight one phrase.
// If they ever stop reassembling the source-of-truth string, the rendered
// subhead silently diverges from the copy every other surface quotes.
it('subhead segments preserve the copy and link each capability to its docs', () => {
expect(HERO_SUBHEAD_SEGMENTS.map((s) => s.text).join('')).toBe(HERO_SUBHEAD);
expect(HERO_SUBHEAD_SEGMENTS.filter((s) => s.highlight)).toHaveLength(1);
expect(HERO_SUBHEAD_SEGMENTS.find((s) => s.highlight)?.text).toBe(
'Your backend stays where it is.',
);
expect(HERO_SUBHEAD_SEGMENTS.filter((s) => s.href)).toEqual([
{ text: 'persistent', href: '/docs/langgraph/guides/persistence' },
{ text: 'durable', href: '/docs/langgraph/guides/persistence#choosing-a-checkpointer' },
{ text: 'visible', href: '/docs/chat/components/chat-tool-calls' },
{ text: 'reviewable', href: '/docs/langgraph/guides/interrupts' },
{ text: 'resumable', href: '/docs/langgraph/guides/persistence#checkpoint-recovery' },
]);
});

it('pins the hero action labels and the secondary destination', () => {
Expand Down
22 changes: 15 additions & 7 deletions apps/website/src/lib/positioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,30 @@ export const HERO_H1 = 'The open-source thread-plane for agents.';
*/
export const HERO_H1_LINES: readonly string[] = ['The open-source', 'thread-plane', 'for agents.'];
export const HERO_SUBHEAD =
'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.';
'Make agent work persistent, durable, visible, reviewable, and resumable.';

/**
* The subhead, split so Hero.tsx can marker-highlight the boundary claim.
* The subhead, split so each workflow capability links to its documentation.
* HERO_SUBHEAD stays the single source of truth: positioning.spec.ts asserts
* these segments join back to it character for character, so the two cannot
* drift. Exactly one segment is highlighted — a second one in a sentence this
* short reads as decoration and cancels the emphasis.
* drift. Linked terms retain the hero's ink underline treatment.
*/
export interface HeroSubheadSegment {
text: string;
highlight?: boolean;
href?: string;
}
export const HERO_SUBHEAD_SEGMENTS: readonly HeroSubheadSegment[] = [
{ text: 'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. ' },
{ text: 'Your backend stays where it is.', highlight: true },
{ text: 'Make agent work ' },
{ text: 'persistent', href: '/docs/langgraph/guides/persistence' },
{ text: ', ' },
{ text: 'durable', href: '/docs/langgraph/guides/persistence#choosing-a-checkpointer' },
{ text: ', ' },
{ text: 'visible', href: '/docs/chat/components/chat-tool-calls' },
{ text: ', ' },
{ text: 'reviewable', href: '/docs/langgraph/guides/interrupts' },
{ text: ', and ' },
{ text: 'resumable', href: '/docs/langgraph/guides/persistence#checkpoint-recovery' },
{ text: '.' },
];

export const HERO_PRIMARY_LABEL = 'Install Threadplane';
Expand Down
2 changes: 1 addition & 1 deletion apps/website/src/lib/site-metadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('site positioning copy', () => {
expect(LONG_SUBHEAD).toContain('open-source thread-plane for agents');
expect(LONG_SUBHEAD).toContain('LangGraph and AG-UI');
expect(HERO_SUBHEAD).toBe(
'Chat, durable threads, persistence, human approvals, and generative UI for Angular, on LangGraph and AG-UI. Your backend stays where it is.',
'Make agent work persistent, durable, visible, reviewable, and resumable.',
);
expect(POSITIONING_PROOF_POINTS.map((p) => p.label)).toEqual([
'LangGraph + AG-UI',
Expand Down
17 changes: 11 additions & 6 deletions apps/website/src/styles/landing.css
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,21 @@
}
.hero-stack .hero-subhead {
max-width: 40em;
word-spacing: 0.16em;
}
/* The centered hero wraps the highlighted boundary claim mid-phrase — measured
* at 768px it orphaned a lone "Your" in its own pill at the end of line 1, and
* at 1440px it split into two staggered boxes. inline-block makes the phrase
* wrap as one unit, so it drops whole onto its own line instead. max-width is
* the safety valve: below ~360px the phrase is wider than the measure, and
* without it a shrink-to-fit box would overflow the page rather than wrap. */
/* Keep each documentation link and its ink underline together when wrapping. */
.hero-stack .hero-subhead .marker-highlight {
display: inline-block;
max-width: 100%;
text-decoration: none;
word-spacing: normal;
}
.hero-stack .hero-subhead a.marker-highlight:hover {
box-shadow: inset 0 -5px 0 var(--color-ink);
}
.hero-stack .hero-subhead a.marker-highlight:focus-visible {
outline: 2px solid var(--color-ink);
outline-offset: 5px;
}
.hero-stack .hero-cta-row {
justify-content: center;
Expand Down
Loading