Skip to content

Commit 127ab51

Browse files
bloveclaude
andauthored
feat(website): library pages in the homepage manner (#914)
* docs(website): spec for library hero subtitle fixes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * copy(website): retire the last two hero litanies on the library pages /langgraph's subtitle was the site's final six-noun list; /ag-ui's re-enumerated all eight runtimes its own H1 had just counted. The other library-page copy was verified already in voice — this closes the recorded five-page copy-pass follow-up as satisfied. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): design spec — library pages in the homepage manner Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(website): implementation plan for library pages Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(website): extract buildPanes for reuse by library pages * feat(website): section media for the library pages Add libLanggraph/libChat/libAgUi/libRender to SECTION_MEDIA for Task 2 of the library-pages plan. Code sources are lifted verbatim from each page's current static mock content (app.config.ts blocks for langgraph/ag-ui, the spec JSON for render); libChat has no code pane since its mock is a fake chat-debug panel, not source. Task 3 wires the pages to these entries and deletes the page-local copies. * feat(website): library pages — switchers, dark closers, package kickers Adopt the homepage's MediumSwitcher, dark FinalCTA, and hero kicker rail across the four library pages (/langgraph, /chat, /ag-ui, /render): - First FeatureBlock's static BrowserFrame mock is replaced with MediumSwitcher, panes built server-side via buildPanes(SECTION_MEDIA.lib*). For /ag-ui the static mock lives on the "primitives" FeatureBlock (its first block, BackendsGrid, is real content, not a mock) — the switcher goes there instead, matching the SECTION_MEDIA.libAgUi code content that was already lifted from that block. - Mono package-identity kicker + rail hairline above each hero h1. - One marker-highlight sweep per subtitle on the load-bearing phrase. - FinalCTA variant="dark" on all four; /ag-ui also gains WhitePaperBlock paper="overview" for parity with the other three. - Orphaned static-mock CSS deleted from pages.css (zero consumers after the page edits): render-page-visual-panel/-spec-block/-rendered-card/ -ai-label/-card-title/-card-value; chat-page-visual-panel/ -visual-pills-row/-tool-label/-result-block/-replay-footer; ag-ui-page-code-pre; langgraph-page-code-pre. Page specs assert kicker text, marker span, switcher tablist, and dark FinalCTA surface. Each spec mocks its second FeatureBlock's *CodeShowcase (untouched, pre-existing) because it's an async Server Component and react-dom/client cannot render an async component outside Next's RSC pipeline ("Only Server Components can be async at the moment"). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(website): single package kicker per library hero Review caught that each library hero (/langgraph, /chat, /ag-ui, /render) showed the package name twice: the new .lib-hero-rail kicker plus the old plain <Eyebrow className="*-page-eyebrow-spaced"> line that predates it. Delete the old line on all four pages, keeping the rail kicker as the sole package-identity marker. Deleted the now-zero-consumer *-page-eyebrow-spaced CSS rules from pages.css: render-page-eyebrow-spaced, chat-page-eyebrow-spaced, ag-ui-page-eyebrow-spaced, langgraph-page-eyebrow-spaced. pricing-page-eyebrow-spaced is a distinct rule still used by /pricing and is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(website): final-review polish for the library pages arc - section-media.ts libRender: SolutionCode's language union is typescript | python | html — no 'json' — so the code block stays tagged typescript (noted inline); simplified its dead label to 'spec.json'. - section-media.ts: the library-entries comment described Task 3's not-yet-done deletion of the page-local mocks; rewritten to present tense now that the pages consume these entries and the mocks are gone. Also documented the /ag-ui deviation (switcher lands on its second block, BackendsGrid stays on the first) directly on libAgUi. - FinalCTA.spec.tsx: test name said "8 non-home pages" — now 4 after the library pages moved to the dark closer. - library-pages-design.md §1: amended the FIRST FeatureBlock sentence with the ratified /ag-ui deviation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 8c48de1 commit 127ab51

16 files changed

Lines changed: 572 additions & 295 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import AgUiPage from './page';
4+
5+
vi.mock('../../lib/analytics/client', () => ({
6+
track: vi.fn(),
7+
trackCtaClick: vi.fn(),
8+
trackExternalLinkClick: vi.fn(),
9+
trackWhitepaperDownloadClick: vi.fn(),
10+
}));
11+
12+
describe('AgUiPage', () => {
13+
it('renders the package kicker, marker sweep, switcher, and dark closer', async () => {
14+
const ui = await AgUiPage();
15+
const { container } = render(ui);
16+
expect(screen.getByText('@threadplane/ag-ui · protocol adapter')).toBeTruthy();
17+
expect(container.querySelector('.marker-highlight')?.textContent).toBe('work the day they ship');
18+
expect(screen.getAllByRole('tablist').length).toBeGreaterThanOrEqual(1);
19+
const cta = [...container.querySelectorAll('[data-ui="section"]')].find((s) =>
20+
s.querySelector('.final-cta-inner'),
21+
);
22+
expect(cta?.getAttribute('data-surface')).toBe('dark');
23+
});
24+
});

apps/website/src/app/ag-ui/page.tsx

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import { Section } from '../../components/ui/Section';
44
import { Eyebrow } from '../../components/ui/Eyebrow';
55
import { Button } from '../../components/ui/Button';
66
import { Pill } from '../../components/ui/Pill';
7-
import { BrowserFrame } from '../../components/ui/BrowserFrame';
87
import { FeatureBlock } from '../../components/landing/FeatureBlock';
8+
import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock';
99
import { FinalCTA } from '../../components/landing/FinalCTA';
10+
import { MediumSwitcher } from '../../components/landing/MediumSwitcher';
1011
import { BackendsGrid } from '../../components/landing/ag-ui/BackendsGrid';
1112
import { createPageMetadata, SHORT_POSITIONING_DESCRIPTION } from '../../lib/site-metadata';
13+
import { SECTION_MEDIA } from '../../lib/section-media';
14+
import { buildPanes } from '../../lib/build-panes';
1215

1316
export const metadata = createPageMetadata({
1417
title: '@threadplane/ag-ui — Threadplane',
@@ -18,18 +21,23 @@ export const metadata = createPageMetadata({
1821
});
1922

2023
export default async function AgUiPage() {
24+
const panes = await buildPanes(SECTION_MEDIA.libAgUi, SECTION_MEDIA.libAgUi.video?.url ?? '');
25+
2126
return (
2227
<>
2328
{/* Hero */}
2429
<Section surface="canvas" ariaLabelledBy="ag-ui-hero-heading">
2530
<Container>
2631
<div className="ag-ui-page-hero-inner">
27-
<Eyebrow tone="accent" className="ag-ui-page-eyebrow-spaced">@threadplane/ag-ui</Eyebrow>
32+
<div className="lib-hero-rail">
33+
<Eyebrow tone="accent">@threadplane/ag-ui · protocol adapter</Eyebrow>
34+
<span className="lib-hero-rail-line" aria-hidden="true" />
35+
</div>
2836
<h1 id="ag-ui-hero-heading" className="ag-ui-page-h1">
2937
One adapter. Eight backends.
3038
</h1>
3139
<p className="ag-ui-page-hero-subtitle">
32-
Build an Angular agent UI on AG-UI-compatible runtimes including CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, or LangGraph fronted by AG-UI. Same Agent contract and chat surface; runtime-specific history and checkpoint behavior stays with the backend.
40+
Build the Angular UI once, on the AG-UI protocol — eight runtimes speak it today, and new ones <span className="marker-highlight">work the day they ship</span>. History and checkpoint behavior stays with your backend.
3341
</p>
3442
<div className="ag-ui-page-hero-buttons">
3543
<Button variant="primary" size="lg" href="/docs/ag-ui/getting-started/quickstart">Get started</Button>
@@ -80,35 +88,11 @@ export default async function AgUiPage() {
8088
]}
8189
cta={{ label: 'API reference', href: '/docs/langgraph/api/inject-agent' }}
8290
visualLeft
83-
visual={
84-
<BrowserFrame url="app.config.ts" elevation="md">
85-
<pre className="ag-ui-page-code-pre">
86-
{`import { provideAgent, injectAgent } from '@threadplane/ag-ui';
87-
import { ChatComponent } from '@threadplane/chat';
88-
89-
// app.config.ts
90-
export const appConfig: ApplicationConfig = {
91-
providers: [
92-
provideAgent({
93-
url: 'https://your.agent.endpoint',
94-
}),
95-
],
96-
};
97-
98-
// component
99-
@Component({
100-
imports: [ChatComponent],
101-
template: \`<chat [agent]="agent" />\`,
102-
})
103-
export class App {
104-
protected readonly agent = injectAgent();
105-
}`}
106-
</pre>
107-
</BrowserFrame>
108-
}
91+
visual={<MediumSwitcher sectionId="lib-ag-ui" panes={panes} />}
10992
/>
11093

111-
<FinalCTA />
94+
<WhitePaperBlock paper="overview" />
95+
<FinalCTA variant="dark" />
11296
</>
11397
);
11498
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import ChatPage from './page';
4+
5+
vi.mock('../../lib/analytics/client', () => ({
6+
track: vi.fn(),
7+
trackCtaClick: vi.fn(),
8+
trackExternalLinkClick: vi.fn(),
9+
trackWhitepaperDownloadClick: vi.fn(),
10+
}));
11+
12+
// The second FeatureBlock's showcase is an async Server Component (it awaits
13+
// shiki highlighting via HighlightedCode). react-dom/client cannot render an
14+
// async component at all outside Next's RSC pipeline ("Only Server
15+
// Components can be async at the moment") — stand in a sync placeholder so
16+
// the rest of the page (untouched by this task) can render in this harness.
17+
vi.mock('../../components/landing/chat-landing/ChatLandingCodeShowcase', () => ({
18+
ChatLandingCodeShowcase: () => null,
19+
}));
20+
21+
describe('ChatPage', () => {
22+
it('renders the package kicker, marker sweep, switcher, and dark closer', async () => {
23+
const ui = await ChatPage();
24+
const { container } = render(ui);
25+
expect(screen.getByText('@threadplane/chat · chat compositions')).toBeTruthy();
26+
expect(container.querySelector('.marker-highlight')?.textContent).toBe('Production-shaped from day one');
27+
expect(screen.getAllByRole('tablist').length).toBeGreaterThanOrEqual(1);
28+
const cta = [...container.querySelectorAll('[data-ui="section"]')].find((s) =>
29+
s.querySelector('.final-cta-inner'),
30+
);
31+
expect(cta?.getAttribute('data-surface')).toBe('dark');
32+
});
33+
});

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

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Section } from '../../components/ui/Section';
33
import { Eyebrow } from '../../components/ui/Eyebrow';
44
import { Button } from '../../components/ui/Button';
55
import { Pill } from '../../components/ui/Pill';
6-
import { BrowserFrame } from '../../components/ui/BrowserFrame';
76
import { FeatureBlock } from '../../components/landing/FeatureBlock';
87
import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock';
98
import { FinalCTA } from '../../components/landing/FinalCTA';
9+
import { MediumSwitcher } from '../../components/landing/MediumSwitcher';
1010
import { ChatLandingCodeShowcase } from '../../components/landing/chat-landing/ChatLandingCodeShowcase';
1111
import { createPageMetadata } from '../../lib/site-metadata';
12+
import { SECTION_MEDIA } from '../../lib/section-media';
13+
import { buildPanes } from '../../lib/build-panes';
1214

1315
export const metadata = createPageMetadata({
1416
title: '@threadplane/chat — Batteries-Included Agent Chat for Angular',
@@ -18,18 +20,23 @@ export const metadata = createPageMetadata({
1820
});
1921

2022
export default async function ChatPage() {
23+
const panes = await buildPanes(SECTION_MEDIA.libChat, SECTION_MEDIA.libChat.video?.url ?? '');
24+
2125
return (
2226
<>
2327
{/* Hero */}
2428
<Section surface="canvas" ariaLabelledBy="chat-hero-heading">
2529
<Container>
2630
<div className="chat-page-hero-inner">
27-
<Eyebrow tone="accent" className="chat-page-eyebrow-spaced">@threadplane/chat</Eyebrow>
31+
<div className="lib-hero-rail">
32+
<Eyebrow tone="accent">@threadplane/chat · chat compositions</Eyebrow>
33+
<span className="lib-hero-rail-line" aria-hidden="true" />
34+
</div>
2835
<h1 id="chat-hero-heading" className="chat-page-h1">
2936
Drop-in chat for Angular agents.
3037
</h1>
3138
<p className="chat-page-hero-subtitle">
32-
chat-timeline + chat-debug + GenUI surfaces. Production-shaped from day one, themable to your design system, or use the headless primitives if you want full control.
39+
chat-timeline + chat-debug + GenUI surfaces. <span className="marker-highlight">Production-shaped from day one</span>, themable to your design system, or use the headless primitives if you want full control.
3340
</p>
3441
<div className="chat-page-hero-buttons">
3542
<Button variant="primary" size="lg" href="/docs/chat/getting-started/introduction">Get started</Button>
@@ -57,26 +64,7 @@ export default async function ChatPage() {
5764
{ claim: 'Thread navigation and history search', api: 'sidenav + palette' },
5865
]}
5966
cta={{ label: 'See @threadplane/chat docs', href: '/docs/chat/getting-started/introduction' }}
60-
visual={
61-
<BrowserFrame url="cockpit.threadplane.ai/chat/core-capabilities/debug/overview/python" elevation="md">
62-
<div className="chat-page-visual-panel">
63-
<div className="chat-page-visual-pills-row">
64-
<Pill variant="accent">streaming</Pill>
65-
<Pill variant="neutral">3 tools</Pill>
66-
<Pill variant="neutral">1 interrupt</Pill>
67-
</div>
68-
<div className="chat-page-tool-label">
69-
tool · query_inventory · 240ms
70-
</div>
71-
<div className="chat-page-result-block">
72-
{`{ items: 47, low_stock: 3, total_value: 12400 }`}
73-
</div>
74-
<div className="chat-page-replay-footer">
75-
replay · 0:24 · paused on interrupt
76-
</div>
77-
</div>
78-
</BrowserFrame>
79-
}
67+
visual={<MediumSwitcher sectionId="lib-chat" panes={panes} />}
8068
/>
8169

8270
<FeatureBlock
@@ -95,7 +83,7 @@ export default async function ChatPage() {
9583
/>
9684

9785
<WhitePaperBlock paper="chat" />
98-
<FinalCTA />
86+
<FinalCTA variant="dark" />
9987
</>
10088
);
10189
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, it, expect, vi } from 'vitest';
3+
import LangGraphPage from './page';
4+
5+
vi.mock('../../lib/analytics/client', () => ({
6+
track: vi.fn(),
7+
trackCtaClick: vi.fn(),
8+
trackExternalLinkClick: vi.fn(),
9+
trackWhitepaperDownloadClick: vi.fn(),
10+
}));
11+
12+
// The second FeatureBlock's showcase is an async Server Component (it awaits
13+
// shiki highlighting via HighlightedCode). react-dom/client cannot render an
14+
// async component at all outside Next's RSC pipeline ("Only Server
15+
// Components can be async at the moment") — stand in a sync placeholder so
16+
// the rest of the page (untouched by this task) can render in this harness.
17+
vi.mock('../../components/landing/langgraph/LangGraphCodeShowcase', () => ({
18+
LangGraphCodeShowcase: () => null,
19+
}));
20+
21+
describe('LangGraphPage', () => {
22+
it('renders the package kicker, marker sweep, switcher, and dark closer', async () => {
23+
const ui = await LangGraphPage();
24+
const { container } = render(ui);
25+
expect(screen.getByText('@threadplane/langgraph · LangGraph adapter')).toBeTruthy();
26+
expect(container.querySelector('.marker-highlight')?.textContent).toBe('humans stay in the loop');
27+
expect(screen.getAllByRole('tablist').length).toBeGreaterThanOrEqual(1);
28+
const cta = [...container.querySelectorAll('[data-ui="section"]')].find((s) =>
29+
s.querySelector('.final-cta-inner'),
30+
);
31+
expect(cta?.getAttribute('data-surface')).toBe('dark');
32+
});
33+
});

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

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { Section } from '../../components/ui/Section';
33
import { Eyebrow } from '../../components/ui/Eyebrow';
44
import { Button } from '../../components/ui/Button';
55
import { Pill } from '../../components/ui/Pill';
6-
import { BrowserFrame } from '../../components/ui/BrowserFrame';
76
import { FeatureBlock } from '../../components/landing/FeatureBlock';
87
import { WhitePaperBlock } from '../../components/landing/WhitePaperBlock';
98
import { FinalCTA } from '../../components/landing/FinalCTA';
9+
import { MediumSwitcher } from '../../components/landing/MediumSwitcher';
1010
import { LangGraphCodeShowcase } from '../../components/landing/langgraph/LangGraphCodeShowcase';
1111
import { createPageMetadata, SHORT_POSITIONING_DESCRIPTION } from '../../lib/site-metadata';
12+
import { SECTION_MEDIA } from '../../lib/section-media';
13+
import { buildPanes } from '../../lib/build-panes';
1214

1315
export const metadata = createPageMetadata({
1416
title: '@threadplane/langgraph — Threadplane',
@@ -18,18 +20,23 @@ export const metadata = createPageMetadata({
1820
});
1921

2022
export default async function LangGraphPage() {
23+
const panes = await buildPanes(SECTION_MEDIA.libLanggraph, SECTION_MEDIA.libLanggraph.video?.url ?? '');
24+
2125
return (
2226
<>
2327
{/* Hero */}
2428
<Section surface="canvas" ariaLabelledBy="angular-hero-heading">
2529
<Container>
2630
<div className="langgraph-page-hero-inner">
27-
<Eyebrow tone="angular" className="langgraph-page-eyebrow-spaced">@threadplane/langgraph</Eyebrow>
31+
<div className="lib-hero-rail">
32+
<Eyebrow tone="accent">@threadplane/langgraph · LangGraph adapter</Eyebrow>
33+
<span className="lib-hero-rail-line" aria-hidden="true" />
34+
</div>
2835
<h1 id="angular-hero-heading" className="langgraph-page-h1">
2936
LangGraph agent UI for Angular.
3037
</h1>
3138
<p className="langgraph-page-hero-subtitle">
32-
Ship LangGraph agents inside your Angular 20–22 app with headless chat, durable threads, interrupts, branch/history, tool progress, and generative UI.
39+
Ship LangGraph agents inside your Angular app. Agent state arrives as signals; threads survive reloads; <span className="marker-highlight">humans stay in the loop</span>.
3340
</p>
3441
<div className="langgraph-page-hero-buttons">
3542
<Button variant="primary" size="lg" href="/docs/langgraph/getting-started/introduction">Get started</Button>
@@ -58,29 +65,7 @@ export default async function LangGraphPage() {
5865
{ claim: 'Deterministic tests without a backend', api: 'MockAgentTransport' },
5966
]}
6067
cta={{ label: 'API reference', href: '/docs/langgraph/api/inject-agent' }}
61-
visual={
62-
<BrowserFrame url="app.config.ts" elevation="md">
63-
<pre className="langgraph-page-code-pre">
64-
{`import { provideAgent } from '@threadplane/langgraph';
65-
66-
export const appConfig: ApplicationConfig = {
67-
providers: [
68-
provideAgent({
69-
apiUrl: '/agent',
70-
assistantId: 'my-agent',
71-
}),
72-
],
73-
};
74-
75-
// any component
76-
export class ChatComponent {
77-
agent = injectAgent();
78-
messages = this.agent.messages;
79-
status = this.agent.status;
80-
}`}
81-
</pre>
82-
</BrowserFrame>
83-
}
68+
visual={<MediumSwitcher sectionId="lib-langgraph" panes={panes} />}
8469
/>
8570

8671
<FeatureBlock
@@ -99,7 +84,7 @@ export class ChatComponent {
9984
/>
10085

10186
<WhitePaperBlock paper="angular" />
102-
<FinalCTA />
87+
<FinalCTA variant="dark" />
10388
</>
10489
);
10590
}

0 commit comments

Comments
 (0)