Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d686cea
docs(specs): homepage trust band shrinks and a No-runtime band lands …
blove Sep 12, 2026
0049ed6
docs(plans): homepage trust band and No-runtime band implementation plan
blove Sep 12, 2026
a19c87e
refactor(website): the checklist data keeps only the five airworthine…
blove Sep 12, 2026
a786a39
feat(website): the trust band renders the five airworthiness rows alone
blove Sep 12, 2026
1631800
feat(website): the trust band's aside frames third-party proof
blove Sep 12, 2026
a80859a
docs(specs): record the live rank and score read on 2026-09-11
blove Sep 12, 2026
cf5e4f8
refactor(website): the trust band CSS and guards keep only what the s…
blove Sep 12, 2026
15b41ce
feat(website): single-source the No-runtime band copy and register it…
blove Sep 12, 2026
6f7c4aa
chore(website): register the No-runtime CTA id and tighten its copy pins
blove Sep 12, 2026
166a0de
feat(website): the No-runtime band — two words over two flows
blove Sep 12, 2026
fb7f413
test(website): pin the No-runtime figure name and the direct arrow
blove Sep 12, 2026
43bfc3c
feat(website): the No-runtime band joins the homepage spine after the…
blove Sep 12, 2026
d5d6c8e
test(website): measure the No-runtime flows at desktop and phone widths
blove Sep 12, 2026
a995d7d
feat(website): the homepage FAQ answers whether there is a runtime to…
blove Sep 12, 2026
d6abd11
docs(specs): the flow guard measures a centre line, not a left edge
blove Sep 12, 2026
ec2f0e7
docs(specs): mark the preflight checklist spec superseded
blove Sep 12, 2026
b000739
fix(website): the No-runtime band drops its runway so it does not mer…
blove Sep 12, 2026
5dbd208
chore(website): final-review polish for the trust and No-runtime bands
blove Sep 12, 2026
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
73 changes: 73 additions & 0 deletions apps/website/e2e/home-no-runtime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { test, expect, type Page } from '@playwright/test';

const BAND = '#no-runtime';

/**
* The unit spec proves the flows are drawn from the copy module; this proves
* the drawing holds together in a real layout. Measured, not eyeballed: every
* node in a flow shares that flow's centre line, no node's text overflows its
* box, and on a phone the flows stack instead of squeezing.
*/
async function nodeReport(page: Page) {
return page.evaluate((sel) => {
const flows = Array.from(document.querySelectorAll<HTMLElement>(`${sel} [data-flow]`));
return flows.map((flow) => {
const nodes = Array.from(flow.querySelectorAll<HTMLElement>('.no-runtime-node'));
return {
id: flow.dataset['flow'],
top: flow.getBoundingClientRect().top,
centres: nodes.map((n) => {
const r = n.getBoundingClientRect();
return Math.round((r.left + r.right) / 2);
}),
overflowing: nodes.filter((n) => n.scrollWidth > n.clientWidth).map((n) => n.textContent),
};
});
}, BAND);
}

test.describe('homepage No-runtime band', () => {
test('aligns every node on its flow centre line and clips no text at 1440', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/');
await expect(page.locator('#no-runtime-heading')).toHaveText('No runtime.');
await page.locator(BAND).scrollIntoViewIfNeeded();
const flows = await nodeReport(page);
expect(flows.map((f) => f.id)).toEqual(['usual', 'ours']);
for (const f of flows) {
expect(f.overflowing, f.id).toEqual([]);
expect(new Set(f.centres).size, `${f.id} centres ${f.centres.join(',')}`).toBe(1);
}
// Side by side: the two flows share a top edge.
expect(Math.abs(flows[0].top - flows[1].top)).toBeLessThanOrEqual(1);
// The ghost hop exists exactly once and only in the usual flow.
await expect(page.locator(`${BAND} .no-runtime-node.is-ghost`)).toHaveCount(1);
await expect(page.locator(`${BAND} [data-flow="ours"] .is-ghost`)).toHaveCount(0);
});

test('stacks the flows and still fits every node on a phone', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
await page.locator(BAND).scrollIntoViewIfNeeded();
const flows = await nodeReport(page);
for (const f of flows) {
expect(f.overflowing, f.id).toEqual([]);
expect(new Set(f.centres).size).toBe(1);
}
// Stacked: the ours flow starts below the usual one.
expect(flows[1].top).toBeGreaterThan(flows[0].top + 100);
// The page never scrolls sideways.
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(overflow).toBeLessThanOrEqual(0);
});

test('carries one link, to the adapter guide, and no button', async ({ page }) => {
await page.goto('/');
const links = page.locator(`${BAND} a`);
await expect(links).toHaveCount(1);
await expect(links).toHaveAttribute('href', '/docs/choosing-an-adapter');
await expect(page.locator(`${BAND} [data-ui="button"]`)).toHaveCount(0);
});
});
1 change: 1 addition & 0 deletions apps/website/e2e/website.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ test('landing page renders the spine in order (live-stage spec §3)', async ({ p
'proof-heading',
'compatibility-heading',
'architecture-heading',
'no-runtime-heading',
'open-source-heading',
'stage-heading',
'field-report-heading',
Expand Down
7 changes: 7 additions & 0 deletions apps/website/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Stage } from '../components/landing/Stage';
import { TeamsBlock } from '../components/landing/TeamsBlock';
import { HomeFAQ } from '../components/landing/HomeFAQ';
import { OpenSourceStrip } from '../components/landing/OpenSourceStrip';
import { NoRuntimeBand } from '../components/landing/NoRuntimeBand';
import { RecentArticles } from '../components/landing/RecentArticles';
// 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
Expand Down Expand Up @@ -35,6 +36,12 @@ export default function HomePage() {
<Compatibility />
<EnterpriseArchitecture />

{/* No runtime: the diagram above shows Threadplane reaching your agent
directly; this band says so in two words. Copy lives in
NO_RUNTIME_BAND (positioning.ts). Deliberately dark-on-dark with the
band below; each dark section paints its own seam (spec §5). */}
<NoRuntimeBand />

{/* The open-source full stop: a loud dark band with one fork CTA. Copy
lives in OPEN_SOURCE_STRIP (positioning.ts). */}
<OpenSourceStrip />
Expand Down
14 changes: 12 additions & 2 deletions apps/website/src/components/landing/HomeFAQ.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ describe('HomeFAQ', () => {
'Can I use my existing Angular component library and design system?',
'Does generated UI execute arbitrary code?',
'Does Threadplane require a hosted service or an account?',
'Does Threadplane have a runtime I need to deploy?',
];
for (const q of questions) expect(screen.getByText(q)).toBeTruthy();
expect(screen.queryByText('Does Threadplane require LangGraph?')).toBeNull();
expect(screen.queryByText(/raw streaming SDK/)).toBeNull();
expect(container.querySelectorAll('summary')).toHaveLength(4);
expect(container.querySelectorAll('a')).toHaveLength(4);
expect(container.querySelectorAll('summary')).toHaveLength(5);
expect(container.querySelectorAll('a')).toHaveLength(5);
});

it('answers the runtime question in the band’s own words', () => {
render(<HomeFAQ />);
const answer = screen.getByText(/no Threadplane server in the request path/);
expect(answer.textContent).toMatch(/no key/);
expect(answer.textContent).toMatch(/no production tier/);
expect(answer.textContent).not.toMatch(/proxy/i);
expect(answer.querySelector('a')?.getAttribute('href')).toBe('/docs/choosing-an-adapter');
});
});
13 changes: 12 additions & 1 deletion apps/website/src/components/landing/HomeFAQ.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Section } from '../ui/Section';
import { Eyebrow } from '../ui/Eyebrow';
import { FAQ, type FAQItem } from '../ui/FAQ';

// Four questions the page above does not answer (live-stage spec §3). Copy here
// Five questions the page above does not answer (live-stage spec §3; the
// runtime one from spec 2026-09-11). Copy here
// is scanned by lib/public-copy-contract.ts: absolute claims ("installation is
// inert") and retired routes (/docs/telemetry/**) are barred — a previous FAQ
// answer shipped both and only the production crawl caught it.
Expand Down Expand Up @@ -46,6 +47,16 @@ const ITEMS: FAQItem[] = [
</>
),
},
{
q: 'Does Threadplane have a runtime I need to deploy?',
a: (
<>
No. The adapters call your LangGraph or AG-UI server from the browser. There is no
Threadplane server in the request path, no key, and no production tier.{' '}
<a href="/docs/choosing-an-adapter">How it is wired</a>
</>
),
},
];

export function HomeFAQ() {
Expand Down
102 changes: 102 additions & 0 deletions apps/website/src/components/landing/NoRuntimeBand.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { NoRuntimeBand } from './NoRuntimeBand';
import { NO_RUNTIME_BAND } 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('NoRuntimeBand', () => {
it('makes the two-word headline the section heading and names the section by it', () => {
const { container } = render(<NoRuntimeBand />);
const heading = screen.getByRole('heading', { level: 2 });
expect(heading.textContent).toBe(NO_RUNTIME_BAND.headline);
expect(heading.id).toBe('no-runtime-heading');
const section = container.querySelector('[data-ui="section"]');
expect(section?.getAttribute('aria-labelledby')).toBe('no-runtime-heading');
expect(section?.getAttribute('id')).toBe('no-runtime');
expect(container.querySelector('.no-runtime-eyebrow')?.textContent).toBe(NO_RUNTIME_BAND.eyebrow);
expect(screen.getByText(NO_RUNTIME_BAND.body)).toBeTruthy();
});

it('sits on the dark surface at the full rhythm, like Fork us', () => {
const { container } = render(<NoRuntimeBand />);
const section = container.querySelector('[data-ui="section"]');
expect(section?.getAttribute('data-surface')).toBe('dark');
expect(section?.getAttribute('data-tight')).toBeNull();
expect(section?.classList.contains('no-runtime')).toBe(true);
});

it('offers one text link and no button, and reports it as a homepage CTA', () => {
const { container } = render(<NoRuntimeBand />);
const links = container.querySelectorAll('a');
expect(links).toHaveLength(1);
const link = screen.getByRole('link', { name: NO_RUNTIME_BAND.link.label });
expect(link.getAttribute('href')).toBe(NO_RUNTIME_BAND.link.href);
expect(container.querySelector('[data-ui="button"]')).toBeNull();
fireEvent.click(link);
expect(trackCtaClickMock).toHaveBeenCalledWith({
cta_id: 'home_no_runtime_docs',
track: 'developer',
surface: 'home',
destination_url: NO_RUNTIME_BAND.link.href,
});
});

it('draws both flows from the copy module, with the ghost hop only in the usual one', () => {
const { container } = render(<NoRuntimeBand />);
const usual = container.querySelector('[data-flow="usual"]');
const ours = container.querySelector('[data-flow="ours"]');
expect(
Array.from(usual!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent),
).toEqual([...NO_RUNTIME_BAND.flows.usual.nodes]);
expect(
Array.from(ours!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent),
).toEqual([...NO_RUNTIME_BAND.flows.ours.nodes]);
expect(container.querySelectorAll('.no-runtime-node.is-ghost')).toHaveLength(1);
expect(usual!.querySelector('.no-runtime-node.is-ghost')?.textContent).toBe(
NO_RUNTIME_BAND.flows.usual.ghost,
);
expect(ours!.querySelector('.is-ghost')).toBeNull();
// The direct arrow spans the hop that is not there: one in ours, none in usual.
const oursArrows = ours!.querySelectorAll('.no-runtime-arrow');
expect(oursArrows).toHaveLength(1);
expect(oursArrows[0].classList.contains('is-direct')).toBe(true);
const usualArrows = usual!.querySelectorAll('.no-runtime-arrow');
expect(usualArrows).toHaveLength(2);
expect(usual!.querySelector('.no-runtime-arrow.is-direct')).toBeNull();
});

it('gives the flows a prose caption and hides the arrows from assistive tech', () => {
const { container } = render(<NoRuntimeBand />);
const figure = container.querySelector('figure.no-runtime-figure');
expect(figure).toBeTruthy();
expect(figure?.querySelector('figcaption')?.textContent).toBe(NO_RUNTIME_BAND.figureCaption);
// getByRole skips hidden subtrees, so aria-hiding the figure fails here.
// (jsdom's accessible-name computation gives a figure no name from its
// figcaption, so the name is not queried; the caption is checked below.)
expect(screen.getByRole('figure')).toBe(figure);
expect(figure?.querySelector('figcaption')?.closest('[aria-hidden="true"]')).toBeNull();
// The drawn flows duplicate the caption, so they are hidden as a unit.
expect(figure?.querySelector('.no-runtime-flows')?.getAttribute('aria-hidden')).toBe('true');
});

it('carries no runway of its own, so it does not merge with Fork us below it', () => {
// Two stripes 450px apart read as one tall block in the browser; the
// marking stays on Fork us alone (spec §5 fallback).
const { container } = render(<NoRuntimeBand />);
expect(container.querySelector('.no-runtime-runway')).toBeNull();
// Structurally, not by class name: the section holds the container and
// nothing else, so no stripe can come back under another name.
const section = container.querySelector('[data-ui="section"]');
expect(section?.children).toHaveLength(1);
expect(section?.firstElementChild?.getAttribute('data-ui')).toBe('container');
});
});
97 changes: 97 additions & 0 deletions apps/website/src/components/landing/NoRuntimeBand.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client';

import { Container } from '../ui/Container';
import { Section } from '../ui/Section';
import { trackCtaClick } from '../../lib/analytics/client';
import { NO_RUNTIME_BAND } from '../../lib/positioning';

/**
* The No-runtime band (spec 2026-09-11): a dark band between the architecture
* diagram and Fork us, in the same loud register. Copy left, two vertical
* flows right — the usual path with a vendor runtime in the middle, and ours
* without it.
*
* No runway stripe here, on purpose. It was in the design; in the browser two
* stripes 450px apart made this band and Fork us read as one tall block
* (spec §5 fallback). Fork us keeps the marking, so the page still has one.
*
* Everything it says comes from NO_RUNTIME_BAND (positioning.ts). It names no
* other product: the band states Threadplane's shape and leaves the
* comparison to the reader.
*
* `'use client'` only for the analytics click handler; there is no state.
*/
export function NoRuntimeBand() {
const { eyebrow, headline, body, link, flows, figureCaption } = NO_RUNTIME_BAND;

return (
<Section surface="dark" id="no-runtime" ariaLabelledBy="no-runtime-heading" className="no-runtime">
<Container>
<div className="no-runtime-grid">
<div className="no-runtime-copy">
<p className="no-runtime-eyebrow">{eyebrow}</p>
<h2 id="no-runtime-heading" className="no-runtime-headline">
{headline}
</h2>
<p className="no-runtime-body">{body}</p>
<a
className="no-runtime-link"
href={link.href}
onClick={() =>
trackCtaClick({
cta_id: 'home_no_runtime_docs',
track: 'developer',
surface: 'home',
destination_url: link.href,
})
}
>
{link.label}
<span aria-hidden="true"> →</span>
</a>
</div>

<figure className="no-runtime-figure">
{/* The drawn flows and the caption say the same thing; the drawing
is hidden as a unit so a screen reader hears it once. */}
<div className="no-runtime-flows" aria-hidden="true">
<Flow id="usual" label={flows.usual.label} nodes={flows.usual.nodes} ghost={flows.usual.ghost} />
<Flow id="ours" label={flows.ours.label} nodes={flows.ours.nodes} />
</div>
<figcaption className="no-runtime-caption">{figureCaption}</figcaption>
</figure>
</div>
</Container>
</Section>
);
}

function Flow({
id,
label,
nodes,
ghost,
}: {
id: 'usual' | 'ours';
label: string;
nodes: readonly string[];
ghost?: string;
}) {
return (
<div className="no-runtime-flow" data-flow={id}>
<p className="no-runtime-flow-label">{label}</p>
{nodes.map((node, i) => (
<div className="no-runtime-hop" key={node}>
{i > 0 ? <span className={`no-runtime-arrow${id === 'ours' ? ' is-direct' : ''}`} /> : null}
<span
className={`no-runtime-node${node === ghost ? ' is-ghost' : ''}${
i === nodes.length - 1 ? ' is-agent' : ''
}`}
>
{node}
</span>
</div>
))}
</div>
);
}
Loading
Loading