From d2f8c81584836f638b61e065609909f3c33e6d23 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:11:01 -0700 Subject: [PATCH 01/10] =?UTF-8?q?docs:=20spec=20=E2=80=94=20For=20teams=20?= =?UTF-8?q?becomes=20the=20briefing=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- .../specs/2026-09-08-teams-briefing-design.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-08-teams-briefing-design.md diff --git a/docs/superpowers/specs/2026-09-08-teams-briefing-design.md b/docs/superpowers/specs/2026-09-08-teams-briefing-design.md new file mode 100644 index 000000000..6c3e1f508 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-teams-briefing-design.md @@ -0,0 +1,115 @@ +# "For teams" becomes the briefing block + +**Date:** 2026-09-08 +**Status:** Approved, ready for planning +**Component:** `apps/website/src/components/landing/TeamsBlock.tsx` +**Follows:** `2026-09-08-fork-us-band-design.md` (the band directly above it) + +## 1. Why + +The section's job is the field report download, with contact as the secondary ask. Measured on the live page at 1440px, it does the opposite of that: + +| | | +| --- | --- | +| Section height | 916px | +| The 01–04 timeline | starts at y=115 — the top of the aside | +| Field report card | y=445 | +| **The form** | **y=618** | +| "Talk to an engineer" | y=667, 8,789px² | +| "Get the field report" | 8,525px² | + +Three problems. The **primary conversion goal sits two-thirds down the section**. The two CTAs are within 3% of each other in visual weight, so nothing signals which ask matters. And the most valuable real estate — the top of the aside — is spent on a four-step timeline that `/pilot-to-prod` already covers in full, with week numbers. + +## 2. The copy was also wrong + +Checked against the shipped `apps/website/public/whitepaper.pdf`: + +| Claimed | Actual | +| --- | --- | +| "18 pages" (3 places) | **17 pages** — `pdfinfo` and a page-break count agree | +| "Error boundaries, fallbacks, observability, deploy" | **`error boundar` 0 hits, `fallback` 0 hits, `observab` 0 hits** in the document | + +The six chapters are: **Streaming State Management · Thread Persistence · Tool-Call Rendering · Human Approval Flows · Generative UI · Deterministic Testing.** + +Three of the four topics named in exchange for an email address appear nowhere in what gets sent. That is the strongest argument for the design below: **the briefing preview renders the real table of contents**, so the claim and the artifact cannot drift apart again. + +## 3. Shape + +Two elements side by side, with the secondary ask beneath both: + +``` +PREFLIGHT BRIEFING · 17 PAGES · FREE ┌─────────────────────┐ + │ THREADPLANE · OSS │ +What breaks between │ From Prototype │ +a demo and production. │ to Production │ + │ CONTENTS │ +[ you@company.com ] [ Get the field report ]│ 01 Streaming State… │ + │ 02 Thread Persist… │ + │ …06 Deterministic… │ +────────────────────────────────────────────└─────────────────────┘ +Shipping inside a large Angular platform? … Talk to an engineer → +``` + +Grid is `minmax(0, 1fr) 400px` with a 64px gap, centre-aligned, collapsing to one column at 900px. The teams line spans the full width beneath, above a `1px` top border. + +**The heading sells; the cover identifies.** The `

` is *"What breaks between a demo and production."* — the report's own title lives on the cover, so the heading earns its size instead of restating the artwork beside it. + +**No description line.** The eyebrow gives length and price, the heading gives the value, the cover gives the contents. A fourth line would repeat one of them. + +## 4. The briefing preview + +Reuses the existing `.wp-paper` visual language from `WhitePaperBlock` — the same `rotate(-1.2deg)`, border, radius and two-layer shadow — so it reads as the same object the library pages already show. What changes is the content: instead of a badge and one line of description, it renders the document's real front matter and table of contents. + +**It is not `aria-hidden`.** The existing `.wp-cover-wrap` is decorative artwork and hides itself from assistive tech; this one carries the table of contents, which is the actual reason to download. Hiding it would withhold the substance from screen reader users. The cover title is an `

` and the contents are an `
    `, so both join the document outline. + +## 5. Where the facts live, and the guard that makes them un-driftable + +The page count and the six chapter titles are one data module — `apps/website/src/lib/field-report.ts` — read by both the eyebrow and the cover. The eyebrow's "17 pages" is **derived from it, not typed**, so the guard below covers the rendered string as well as the data. + +The page count is asserted against the PDF at test time, dependency-free: + +```ts +const pdf = readFileSync('apps/website/public/whitepaper.pdf', 'latin1'); +const pages = (pdf.match(/\/Type\s*\/Page[^s]/g) || []).length; +expect(FIELD_REPORT.pages).toBe(pages); +``` + +Verified: both this regex and `pdfinfo` return 17. Regenerating the PDF at a different length now fails the suite instead of silently making the homepage lie. + +**The chapter titles cannot be guarded the same way** — the text lives in compressed streams and extracting it needs a dependency CI does not have. They are verified manually; the command is recorded here so the next person does not have to rediscover it: + +```bash +pdftotext -f 1 -l 2 apps/website/public/whitepaper.pdf - | sed -n '1,40p' +``` + +## 6. Scope beyond the section + +`WhitePaperBlock` — used by the four library pages — carries the **same two false claims** (`18 pages`, and `Error boundaries, fallbacks, observability, deploy`). Correcting only the homepage would knowingly leave them live elsewhere, so both strings are fixed there too. The block's layout is otherwise untouched. + +## 7. What gets cut + +- **The 01–04 timeline.** `/pilot-to-prod` covers it in full. The homepage version earned nothing and occupied the top of the aside. +- **The four outcome rows.** They describe the pilot engagement, now carried by one sentence. +- **"See the pilot program" button.** `/pilot-to-prod` is in the primary nav (`Nav.tsx:19`) and the footer, so it is not orphaned. +- **The report card wrapper** (`.teams-report`, `-badge`, `-title`, `-desc`). The report is the section now; it does not need a box inside it. + +**Contact steps down from button to link.** Two equal-weight buttons is what stopped today's section saying which ask matters. + +## 8. Identifiers, tests and analytics + +- The `

    ` id changes from `pilot-heading` to `field-report-heading`, and **`e2e/website.spec.ts:57` is updated in the same commit.** The spine exists to catch accidental removals; this rename is deliberate. It is the only test edit this change makes. +- `teams-report-heading` disappears; nothing outside the component references it. +- The section keeps `id="teams"`, its `tinted` surface and its position in `page.tsx`. +- **Analytics keys are unchanged** so the funnel stays continuous: the form keeps `surface="home_whitepaper"` and `sourceSection="teams-block"`, and the contact link keeps `cta_id: 'hero_talk_to_engineers'` despite becoming a link. +- The form's own disclosure (`formPolicy.disclosures.whitepaper`) and its "Already on the list?" direct-download line are rendered by `WhitePaperForm` and must not be duplicated in the section. + +## 9. Noted, not fixed here + +`WhitePaperBlock` claims *"No vendor pitch — what we learned shipping it."* Per `docs/superpowers/plans/2026-04-05-whitepaper-pipeline.md`, the document's six chapters were generated by a script calling the Anthropic API. "What we learned shipping it" is therefore hard to defend as written. It is **not** carried into the new section, and it is left alone on the library pages rather than quietly reworded, because whether to rewrite it or to rewrite the document is a call for the owner rather than a copy edit. + +## 10. Verification + +- `npx nx test website`, `npx nx lint website`, `npx nx build website`. +- `npx nx e2e website`, with `field-report-heading` in the spine and everything else passing unchanged. +- Measured in a browser, not eyeballed: the form sits **above y≈300** in the section (today it is at 618), the cover's contents list shows six entries, and the heading shares the container's left edge with `#proof-heading`. +- At 390px the grid collapses to one column, the cover does not overflow, and the form's input and button stack without clipping. From c91b41ee36f50c5de50f67117329e1d0a9d5cdc2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:19:30 -0700 Subject: [PATCH 02/10] docs: implementation plan for the teams briefing block Co-Authored-By: Claude Opus 5 --- .../plans/2026-09-08-teams-briefing.md | 819 ++++++++++++++++++ 1 file changed, 819 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-08-teams-briefing.md diff --git a/docs/superpowers/plans/2026-09-08-teams-briefing.md b/docs/superpowers/plans/2026-09-08-teams-briefing.md new file mode 100644 index 000000000..725ff4051 --- /dev/null +++ b/docs/superpowers/plans/2026-09-08-teams-briefing.md @@ -0,0 +1,819 @@ +# Teams Briefing Block Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the homepage "For teams" section so the field-report form sits near the top beside a preview of the actual document, with contact demoted to a link — and correct the two false claims the section makes about that document. + +**Architecture:** The document's facts (page count, six chapter titles, cover strings) move into one data module read by both the eyebrow and a new `FieldReportCover` component, so the page cannot describe the PDF differently from what the PDF is. A test asserts the declared page count against the PDF bytes. `TeamsBlock` loses the duplicated pilot timeline and outcome rows and becomes a two-column ask-plus-artifact. + +**Tech Stack:** Next.js 15 App Router, React, plain CSS in `apps/website/src/styles/landing.css`, Vitest + Testing Library, Playwright. + +**Spec:** `docs/superpowers/specs/2026-09-08-teams-briefing-design.md` + +--- + +## Background an engineer needs before starting + +**Run the commands CI runs**: `npx nx test website`, `npx nx lint website`, `npx nx build website`. NOT `npx vitest run --root apps/website` — the nx executor is what CI uses and it swallows reporter output on a hard failure, so when it goes red run vitest directly alongside it to read the failure, then trust nx for the verdict. `nx test` does **not** type-check; `nx build` does. + +**Free the ports before `nx e2e`.** Two separate e2e failures on the previous change were port 4308 held by an orphaned server and the Browser pane's dev server holding `apps/website/.next/dev/lock` — neither was a test defect. Run `lsof -ti:3000,4308 | xargs kill -9` and stop any preview server first. + +**Facts about the PDF, already verified — do not re-derive, and do not "correct" them:** + +- `apps/website/public/whitepaper.pdf` is **17 pages**. Both `pdfinfo` and counting `/Type /Page` objects agree. The site currently claims 18 in three places. +- The six chapters are **Streaming State Management, Thread Persistence, Tool-Call Rendering, Human Approval Flows, Generative UI, Deterministic Testing**. +- The cover reads `THREADPLANE · OPEN SOURCE · ANGULAR`, then *"Production-ready chat, threads, and generative UI for AI agents"*, then `threadplane.ai · 2026`. +- The strings the site currently advertises — "Error boundaries, fallbacks, observability" — return **zero** matches in the document. + +**`.pilot-eyebrow` is shared with `/pilot-to-prod`.** Every other `.pilot-*` class belongs to `TeamsBlock` alone and can go. Deleting `.pilot-eyebrow` would break another page. + +**Do not touch:** the section's `id="teams"`, its `tinted` surface, its position in `page.tsx`, `WhitePaperForm` itself, or the analytics keys `home_whitepaper`, `teams-block`, `hero_talk_to_engineers`. + +**`WhitePaperForm` already renders its own disclosure and an "Already on the list?" direct-download link.** Do not add fine print of your own beneath the form. + +--- + +## File Structure + +| File | Responsibility | Change | +| --- | --- | --- | +| `apps/website/src/lib/field-report.ts` | The document's facts, single source | **Create** | +| `apps/website/src/lib/field-report.spec.ts` | Asserts the page count against the PDF | **Create** | +| `apps/website/src/components/landing/FieldReportCover.tsx` | The briefing preview | **Create** | +| `apps/website/src/components/landing/FieldReportCover.spec.tsx` | Cover render guards | **Create** | +| `apps/website/src/components/landing/TeamsBlock.tsx` | The section | Rewritten body | +| `apps/website/src/components/landing/TeamsBlock.spec.tsx` | Section guards | Rewritten | +| `apps/website/src/components/landing/WhitePaperBlock.tsx` | Library-page block | Copy corrections only | +| `apps/website/src/styles/landing.css` | Styles | New rules in, `.pilot-*`/`.teams-report-*` out | +| `apps/website/e2e/website.spec.ts:57` | Homepage spine | One id renamed | + +--- + +## Task 1: The document's facts + +**Files:** +- Create: `apps/website/src/lib/field-report.ts` +- Create: `apps/website/src/lib/field-report.spec.ts` + +- [ ] **Step 1: Write the failing spec** + +Create `apps/website/src/lib/field-report.spec.ts`: + +```ts +import { readFileSync } from 'node:fs'; +import { describe, it, expect } from 'vitest'; +import { FIELD_REPORT } from './field-report'; + +describe('FIELD_REPORT', () => { + it('declares the page count the PDF actually has', () => { + // The homepage advertised "18 pages" for a 17-page document, in exchange + // for an email address. Counting the page objects in the file itself is + // dependency-free and means regenerating the PDF at a different length + // fails here instead of silently making the page lie. + const pdf = readFileSync('public/whitepaper.pdf', 'latin1'); + const pages = (pdf.match(/\/Type\s*\/Page[^s]/g) || []).length; + expect(pages).toBeGreaterThan(0); + expect(FIELD_REPORT.pages).toBe(pages); + }); + + it('lists the six chapters the document actually contains', () => { + // Not machine-checkable: the text lives in compressed streams. Verify by + // hand with the command in the module's docblock when the PDF changes. + expect(FIELD_REPORT.chapters).toEqual([ + 'Streaming State Management', + 'Thread Persistence', + 'Tool-Call Rendering', + 'Human Approval Flows', + 'Generative UI', + 'Deterministic Testing', + ]); + }); + + it('carries the cover strings the document prints', () => { + expect(FIELD_REPORT.title).toBe('From Prototype to Production'); + expect(FIELD_REPORT.kicker).toBe('Threadplane · Open source · Angular'); + expect(FIELD_REPORT.subtitle).toBeTruthy(); + expect(FIELD_REPORT.year).toBe('2026'); + }); +}); +``` + +Note the path is `public/whitepaper.pdf`, relative to the vitest root (`apps/website`), not the repo root. + +- [ ] **Step 2: Run it and watch it fail** + +```bash +npx nx test website +``` + +Expected: FAIL — `./field-report` does not exist. + +- [ ] **Step 3: Create the module** + +Create `apps/website/src/lib/field-report.ts`: + +```ts +/** + * The field report's own facts, in one place. + * + * These exist as data because the site got them wrong: it advertised "18 + * pages" for a 17-page document, and described the contents as "Error + * boundaries, fallbacks, observability, deploy" — three phrases that return + * zero matches in the file. Anything the site says about the document now + * reads from here, and `field-report.spec.ts` pins the page count to the PDF + * itself. + * + * To re-verify after regenerating the PDF: + * + * pdftotext -f 1 -l 2 apps/website/public/whitepaper.pdf - | sed -n '1,40p' + */ +export const FIELD_REPORT = { + /** Printed on the cover. */ + title: 'From Prototype to Production', + /** The cover's header rule. */ + kicker: 'Threadplane · Open source · Angular', + /** The cover's standfirst, verbatim. */ + subtitle: 'Production-ready chat, threads, and generative UI for AI agents.', + /** The cover's date line. */ + year: '2026', + /** Verified against the file, and asserted against it in the spec. */ + pages: 17, + /** The table of contents, in document order. */ + chapters: [ + 'Streaming State Management', + 'Thread Persistence', + 'Tool-Call Rendering', + 'Human Approval Flows', + 'Generative UI', + 'Deterministic Testing', + ], +} as const; +``` + +- [ ] **Step 4: Run it** + +```bash +npx nx test website +``` + +Expected: PASS, 3 new tests. + +- [ ] **Step 5: Mutation-test the page guard** + +The guard is the point of this task, so prove it bites. Temporarily change `pages: 17` to `pages: 18` and run: + +```bash +npx vitest run --root apps/website src/lib/field-report.spec.ts +``` + +Expected: FAIL with `expected 18 to be 17`. **Restore `17`** and confirm green before committing. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/lib/field-report.ts apps/website/src/lib/field-report.spec.ts +git commit -m "feat(website): the field report's facts, pinned to the PDF + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Task 2: The briefing preview + +**Files:** +- Create: `apps/website/src/components/landing/FieldReportCover.tsx` +- Create: `apps/website/src/components/landing/FieldReportCover.spec.tsx` + +- [ ] **Step 1: Write the failing spec** + +Create `apps/website/src/components/landing/FieldReportCover.spec.tsx`: + +```tsx +// @vitest-environment jsdom +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { FieldReportCover } from './FieldReportCover'; +import { FIELD_REPORT } from '../../lib/field-report'; + +describe('FieldReportCover', () => { + it('shows every chapter, in order', () => { + const { container } = render(); + const items = Array.from(container.querySelectorAll('.field-report-toc li')); + expect(items.map((li) => li.textContent?.replace(/^\d+/, '').trim())).toEqual([ + ...FIELD_REPORT.chapters, + ]); + }); + + it('is readable rather than decorative', () => { + // The old .wp-cover-wrap is aria-hidden because it is artwork. This one + // carries the table of contents, which is the reason to download — hiding + // it would withhold the substance from screen reader users. + const { container } = render(); + expect(container.querySelector('[aria-hidden="true"].field-report-paper')).toBeNull(); + expect(screen.getByRole('heading', { level: 3 }).textContent).toBe(FIELD_REPORT.title); + expect(container.querySelector('ol.field-report-toc')).toBeTruthy(); + }); + + it('does not read the numbers twice', () => { + // The
      already numbers the list for assistive tech; the drawn 01/02 + // markers are visual duplicates and must be hidden. + const { container } = render(); + const nums = Array.from(container.querySelectorAll('.field-report-num')); + expect(nums).toHaveLength(FIELD_REPORT.chapters.length); + for (const n of nums) expect(n.getAttribute('aria-hidden')).toBe('true'); + }); + + it('borrows the library pages’ paper styling', () => { + // Same object the four library pages already show, so it is not a second + // visual language for the same artifact. + const { container } = render(); + expect(container.querySelector('.wp-paper.field-report-paper')).toBeTruthy(); + }); +}); +``` + +- [ ] **Step 2: Run and watch it fail** + +```bash +npx nx test website +``` + +Expected: FAIL — `./FieldReportCover` does not exist. + +- [ ] **Step 3: Create the component** + +Create `apps/website/src/components/landing/FieldReportCover.tsx`: + +```tsx +import { FIELD_REPORT } from '../../lib/field-report'; + +/** + * The field report rendered as the object it is, contents included. + * + * It reuses `.wp-paper` — the tilted card the library pages already show — + * so the same artifact does not get two visual languages, and adds its own + * classes for the content the library version does not have. + * + * Deliberately NOT aria-hidden. The library page's cover is artwork and hides + * itself; this one prints the table of contents, which is the reason anyone + * gives up an email address for it. + */ +export function FieldReportCover() { + return ( +
      +
      +
      +

      {FIELD_REPORT.kicker}

      +

      {FIELD_REPORT.title}

      +

      {FIELD_REPORT.subtitle}

      +

      Contents

      +
        + {FIELD_REPORT.chapters.map((chapter, i) => ( +
      1. + + {chapter} +
      2. + ))} +
      +
      +

      + threadplane.ai + {FIELD_REPORT.year} +

      +
      +
      + ); +} +``` + +- [ ] **Step 4: Run it** + +```bash +npx nx test website +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/landing/FieldReportCover.tsx apps/website/src/components/landing/FieldReportCover.spec.tsx +git commit -m "feat(website): the briefing preview shows the real contents + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Task 3: The section + +**Files:** +- Modify: `apps/website/src/components/landing/TeamsBlock.tsx` +- Modify: `apps/website/src/components/landing/TeamsBlock.spec.tsx` + +- [ ] **Step 1: Replace the spec** + +Replace the whole `describe('TeamsBlock')` body in `TeamsBlock.spec.tsx` (keep the file's existing imports, the `vi.mock`, and the `formPolicy` fixture exactly as they are, and add `FIELD_REPORT` to the imports): + +```tsx +describe('TeamsBlock', () => { + it('leads with the ask: eyebrow, heading, then the form', () => { + const { container } = render(); + const heading = screen.getByRole('heading', { level: 2 }); + expect(heading.textContent).toBe('What breaks between a demo and production.'); + expect(heading.id).toBe('field-report-heading'); + expect(container.querySelectorAll('form')).toHaveLength(1); + expect(screen.getByLabelText('Work email')).toBeTruthy(); + expect(screen.getByText(formPolicy.disclosures.whitepaper)).toBeTruthy(); + }); + + it('derives the page count rather than typing it', () => { + // The section claimed "18 pages" for a 17-page document. Reading it from + // FIELD_REPORT means the guard against the PDF covers this string too. + const { container } = render(); + const eyebrow = container.querySelector('[data-ui="eyebrow"]'); + expect(eyebrow?.textContent).toContain(`${FIELD_REPORT.pages} pages`); + expect(eyebrow?.textContent).toContain('Preflight briefing'); + }); + + it('shows the briefing beside the ask', () => { + const { container } = render(); + expect(container.querySelector('.field-report-paper')).toBeTruthy(); + }); + + it('makes contact the secondary ask, not a rival button', () => { + // Two same-weight buttons is what stopped the old section saying which + // ask mattered. The arrow is decorative so the accessible name stays clean. + const { container } = render(); + const link = screen.getByRole('link', { name: 'Talk to an engineer' }); + expect(link.getAttribute('href')).toBe('/contact?source=home_enterprise&track=enterprise'); + expect(link.getAttribute('data-ui')).not.toBe('button'); + // Exactly one button in the whole section, and it belongs to the form. + // (SubmitButton wraps Button, so the form's submit carries data-ui too.) + const buttons = container.querySelectorAll('[data-ui="button"]'); + expect(buttons).toHaveLength(1); + expect(buttons[0].closest('form')).toBeTruthy(); + }); + + it('no longer repeats the pilot programme the dedicated page owns', () => { + const { container } = render(); + expect(container.querySelectorAll('.pilot-step')).toHaveLength(0); + expect(container.querySelectorAll('.pilot-row')).toHaveLength(0); + expect(screen.queryByRole('link', { name: 'See the pilot program' })).toBeNull(); + }); + + it('never advertises topics the document does not contain', () => { + // "Error boundaries", "fallbacks" and "observability" each return zero + // matches in whitepaper.pdf. They were on this page for months. + const { container } = render(); + const text = container.textContent ?? ''; + for (const phrase of ['Error boundaries', 'fallbacks', 'observability', '18 pages']) { + expect(text).not.toContain(phrase); + } + }); +}); +``` + +- [ ] **Step 2: Run and watch it fail** + +```bash +npx nx test website +``` + +Expected: FAIL — the heading is still "Shipping inside a large Angular platform?" with id `pilot-heading`. + +- [ ] **Step 3: Rewrite the component** + +Replace the whole of `TeamsBlock.tsx` with: + +```tsx +'use client'; + +import type { PublicFormPolicy } from '../../lib/growth/form-policy'; +import { Container } from '../ui/Container'; +import { Section } from '../ui/Section'; +import { Eyebrow } from '../ui/Eyebrow'; +import { trackCtaClick } from '../../lib/analytics/client'; +import { WhitePaperForm } from './WhitePaperForm'; +import { FieldReportCover } from './FieldReportCover'; +import { FIELD_REPORT } from '../../lib/field-report'; + +/** + * For teams: the field report is the ask, contact is the follow-up. + * + * The previous version inverted its own goals — the form sat at y=618 in a + * 916px section, below a four-step pilot timeline that `/pilot-to-prod` + * already covers in full, while a same-weight amber button sent people to + * contact instead. The timeline and the outcome rows are gone, the form sits + * beside a preview of the actual document, and contact is a link. + * + * Everything the section says about the report reads from FIELD_REPORT, which + * is pinned to the PDF — this block previously advertised a page count and a + * contents list that the file did not match. + */ +export function TeamsBlock({ formPolicy }: { formPolicy: PublicFormPolicy }) { + return ( +
      + +
      +
      + + Preflight briefing + + {' '}· {FIELD_REPORT.pages} pages · free + + +

      + What breaks between a demo and production. +

      + +
      + +
      + +
      +

      + Shipping inside a large Angular platform? Bring your backend, security + model, and design system. +

      + + trackCtaClick({ + cta_id: 'hero_talk_to_engineers', + track: 'enterprise', + surface: 'home', + }) + } + > + Talk to an engineer + +
      +
      +
      + ); +} +``` + +The `TIMELINE` and `OUTCOMES` consts are deleted with the old body. + +- [ ] **Step 4: Run it** + +```bash +npx nx test website +``` + +Expected: PASS. + +- [ ] **Step 5: Update the e2e spine** + +In `apps/website/e2e/website.spec.ts:57`, change `'pilot-heading',` to `'field-report-heading',`. **This is the only test edit this change makes** — the spine catches accidental removals, and this rename is deliberate. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/components/landing/TeamsBlock.tsx apps/website/src/components/landing/TeamsBlock.spec.tsx apps/website/e2e/website.spec.ts +git commit -m "feat(website): For teams leads with the briefing, not the pilot timeline + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Task 4: The same false claims on the library pages + +**Files:** +- Modify: `apps/website/src/components/landing/WhitePaperBlock.tsx:7-11,57` + +`WhitePaperBlock` is used by the four library pages and repeats both errors. Correcting only the homepage would knowingly leave them live. + +- [ ] **Step 1: Fix the rows and the badge** + +Add the import: + +```tsx +import { FIELD_REPORT } from '../../lib/field-report'; +``` + +Replace `ROWS` with: + +```tsx +const ROWS = [ + { claim: 'Six production-readiness dimensions', tail: `${FIELD_REPORT.pages} pages` }, + // Was "Error boundaries, fallbacks, observability, deploy" — three phrases + // with zero matches in whitepaper.pdf. These are the document's actual + // chapters. + { claim: 'Streaming, persistence, tool calls, approvals, generative UI, testing', tail: 'the six chapters' }, + { claim: 'No vendor pitch — what we learned shipping it', tail: 'free' }, +]; +``` + +and the cover badge (line 57) with: + +```tsx +
      Field report · {FIELD_REPORT.pages} pages
      +``` + +Leave the third row alone. Its wording is questioned in spec §9 and is the owner's call, not a copy edit. + +- [ ] **Step 2: Test, lint, build** + +```bash +npx nx test website && npx nx lint website && npx nx build website +``` + +Expected: all pass, 0 lint errors. `WhitePaperBlock.spec.tsx` does not assert any of the changed strings, so it needs no edit. + +- [ ] **Step 3: Confirm the claims are gone site-wide** + +```bash +grep -rn "18 pages\|Error boundaries, fallbacks" apps/website/src || echo "clean" +``` + +Expected: `clean` + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/src/components/landing/WhitePaperBlock.tsx +git commit -m "fix(website): the library pages described a document we do not send + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Task 5: The styles + +**Files:** +- Modify: `apps/website/src/styles/landing.css` + +- [ ] **Step 1: Add the new rules** + +Append to `landing.css`: + +```css +/* For teams — components/landing/TeamsBlock.tsx. + * Ask left, briefing right. The cover is a fixed 400px so the form column + * keeps a comfortable measure rather than stretching across the section. */ +.teams-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 400px; + gap: 64px; + align-items: center; +} +.teams-eyebrow { + margin-bottom: 18px; +} +.teams-eyebrow-meta { + color: var(--color-text-muted); +} +.teams-heading { + font-family: var(--font-display); + font-weight: 400; + font-size: clamp(36px, 4.2vw, 54px); + line-height: 1.02; + letter-spacing: -0.03em; + color: var(--color-text-primary); + margin: 0 0 30px; + max-width: 16ch; +} +.teams-contact { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 14px; + margin-top: 40px; + padding-top: 24px; + border-top: 1px solid var(--color-border); +} +.teams-contact-copy { + margin: 0; + font-size: 15px; + line-height: 1.6; + color: var(--color-text-secondary); +} +.teams-contact-link { + font-size: 15px; + font-weight: 600; + color: var(--color-accent); + text-decoration: none; + white-space: nowrap; +} +.teams-contact-link:hover { + text-decoration: underline; +} + +/* The briefing preview — components/landing/FieldReportCover.tsx. + * .wp-paper supplies the tilt, border and shadow; these rules only style the + * content the library pages' version does not carry. */ +.field-report-cover { + display: flex; + justify-content: center; + min-width: 0; +} +.field-report-paper { + width: 100%; + max-width: 390px; +} +.field-report-kicker { + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-text-muted); + margin: 0 0 14px; + padding-bottom: 14px; + border-bottom: 1px solid var(--color-border); +} +.field-report-title { + font-family: var(--font-display); + font-weight: 400; + font-size: 25px; + line-height: 1.1; + letter-spacing: -0.02em; + color: var(--color-text-primary); + margin: 0 0 6px; +} +.field-report-sub { + font-size: 12.5px; + line-height: 1.5; + color: var(--color-text-secondary); + margin: 0 0 20px; +} +.field-report-toc-label { + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-text-muted); + margin: 0 0 10px; +} +.field-report-toc { + list-style: none; + margin: 0; + padding: 0; +} +.field-report-toc li { + display: flex; + align-items: baseline; + gap: 10px; + padding: 6px 0; + font-size: 12.5px; + color: var(--color-text-primary); +} +.field-report-toc li + li { + border-top: 1px dotted var(--color-border); +} +.field-report-num { + flex: 0 0 auto; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + color: var(--color-signal); +} +.field-report-foot { + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-text-muted); + border-top: 1px solid var(--color-border); + padding-top: 12px; + margin: 20px 0 0; +} +@media (max-width: 900px) { + .teams-grid { + /* minmax(0,…) not 1fr: a bare 1fr keeps an auto minimum, and the cover + would push the stacked column past the viewport. */ + grid-template-columns: minmax(0, 1fr); + gap: 44px; + } + .field-report-cover { + justify-content: flex-start; + } +} +``` + +- [ ] **Step 2: Remove what the section no longer renders** + +Delete these rules from `landing.css`: `.pilot-block-grid`, `.pilot-rail`, `.pilot-rail-line`, `.pilot-heading`, `.pilot-subhead`, `.pilot-cta-row`, `.pilot-rows`, `.pilot-row`, `.pilot-row-claim`, `.pilot-row-tail`, `.pilot-steps`, `.pilot-step`, `.pilot-step:last-child`, `.pilot-step-num`, `.pilot-step-title`, `.pilot-step-body`, `.teams-block-grid`, `.teams-aside`, `.teams-report`, `.teams-report-badge`, `.teams-report-title`, `.teams-report-desc`, plus any `@media` rule that only adjusts one of them. + +**Keep `.pilot-eyebrow`** — `/pilot-to-prod` uses it. Keep every `.wp-*` rule. + +Before deleting each one, confirm it is unused: + +```bash +for c in pilot-block-grid pilot-rail pilot-rail-line pilot-heading pilot-subhead pilot-cta-row pilot-rows pilot-row pilot-row-claim pilot-row-tail pilot-steps pilot-step pilot-step-num pilot-step-title pilot-step-body teams-block-grid teams-aside teams-report teams-report-badge teams-report-title teams-report-desc; do + hits=$(grep -rl "\b$c\b" apps/website/src --include='*.tsx' | grep -v '\.spec\.' | wc -l | tr -d ' ') + [ "$hits" != "0" ] && echo "STILL USED: $c" +done; echo "check complete" +``` + +Expected: `check complete` with no `STILL USED` lines. + +- [ ] **Step 3: Test, lint, build** + +```bash +npx nx test website && npx nx lint website && npx nx build website +``` + +Expected: all pass, 0 lint errors. + +- [ ] **Step 4: Commit** + +```bash +git add apps/website/src/styles/landing.css +git commit -m "feat(website): briefing block styles in, pilot timeline styles out + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Task 6: Verification + +- [ ] **Step 1: The CI commands** + +```bash +npx nx test website +npx nx lint website +npx nx build website +``` + +All pass; 0 lint errors. + +- [ ] **Step 2: e2e** + +```bash +lsof -ti:3000,4308 | xargs kill -9 2>/dev/null +npx nx e2e website +``` + +Stop any Browser pane preview server first — it holds `apps/website/.next/dev/lock` and the run will fail to start a web server. The spine test must pass with `field-report-heading`; everything else must pass unchanged. + +- [ ] **Step 3: Measure the thing the redesign was for** + +Start the dev server through the Browser pane (`website-dev`), restarting it if one is already running. At a 1440px viewport: + +```javascript +const s = document.querySelector('#teams'); +const top = scrollY + s.getBoundingClientRect().top; +const rel = (el) => Math.round(scrollY + el.getBoundingClientRect().top - top); +const form = s.querySelector('form'); +const h = document.querySelector('#field-report-heading'); +({ sectionH: Math.round(s.getBoundingClientRect().height), + formTop: rel(form), + headingLeft: Math.round(h.getBoundingClientRect().left), + proofHeadingLeft: Math.round(document.querySelector('#proof-heading').getBoundingClientRect().left), + chapters: s.querySelectorAll('.field-report-toc li').length, + buttons: s.querySelectorAll('[data-ui="button"]').length, + buttonsOutsideForm: [...s.querySelectorAll('[data-ui="button"]')].filter((b) => !b.closest('form')).length, + pagesClaimed: s.querySelector('[data-ui="eyebrow"]').textContent.match(/(\d+) pages/)?.[1] }) +``` + +Expected: `formTop` **under 320** (it was 618), `chapters` **6**, `buttons` **1** and `buttonsOutsideForm` **0** — the only button is the form's own submit — `pagesClaimed` **"17"**, and `headingLeft` equal to `proofHeadingLeft`. + +- [ ] **Step 4: Check the phone** + +Emulate 390px, reload, then: + +```javascript +({ overflow: document.documentElement.scrollWidth > document.documentElement.clientWidth, + cols: getComputedStyle(document.querySelector('.teams-grid')).gridTemplateColumns }) +``` + +Expected: `overflow` **false**, and `cols` a single track. + +- [ ] **Step 5: Look at it** + +Screenshot the section. Confirm the cover reads as a document rather than a card, the six chapters are legible at their 12.5px, and the form is the first thing the eye lands on. + +- [ ] **Step 6: Commit any fixes** + +```bash +git add -A +git commit -m "fix(website): teams briefing verification pass + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +## Out of scope + +- **`WhitePaperBlock`'s "No vendor pitch — what we learned shipping it."** Spec §9: the document's chapters were script-generated, which makes the line hard to defend, but rewriting it or rewriting the document is the owner's call. +- The section does not move in `page.tsx`, and `/pilot-to-prod` is untouched. +- No change to `WhitePaperForm`, the form policy, or any analytics key. From a3267d532ecdc1dde9b2e276dd9bc05012347c98 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:33:46 -0700 Subject: [PATCH 03/10] feat(website): the field report's facts, pinned to the PDF Co-Authored-By: Claude Opus 5 --- apps/website/src/lib/field-report.spec.ts | 53 +++++++++++++++++++++++ apps/website/src/lib/field-report.ts | 35 +++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 apps/website/src/lib/field-report.spec.ts create mode 100644 apps/website/src/lib/field-report.ts diff --git a/apps/website/src/lib/field-report.spec.ts b/apps/website/src/lib/field-report.spec.ts new file mode 100644 index 000000000..75199f335 --- /dev/null +++ b/apps/website/src/lib/field-report.spec.ts @@ -0,0 +1,53 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { FIELD_REPORT } from './field-report'; + +// Anchored to the workspace root the way the other file-reading specs here +// are, rather than to a bare relative path: both `nx test website` and +// `vitest --root apps/website` run with the repo root as the working +// directory, and jsdom makes `import.meta.url` an http: URL, so neither a +// cwd-relative path nor a module-relative one resolves the file. +const findWorkspaceRoot = (): string => { + let directory = process.cwd(); + while (directory !== resolve(directory, '..')) { + if (existsSync(join(directory, 'nx.json'))) return directory; + directory = resolve(directory, '..'); + } + throw new Error('workspace root (nx.json) not found'); +}; + +const PDF = join(findWorkspaceRoot(), 'apps/website/public/whitepaper.pdf'); + +describe('FIELD_REPORT', () => { + it('declares the page count the PDF actually has', () => { + // The homepage advertised "18 pages" for a 17-page document, in exchange + // for an email address. Counting the page objects in the file itself is + // dependency-free and means regenerating the PDF at a different length + // fails here instead of silently making the page lie. + const pdf = readFileSync(PDF, 'latin1'); + const pages = (pdf.match(/\/Type\s*\/Page[^s]/g) || []).length; + expect(pages).toBeGreaterThan(0); + expect(FIELD_REPORT.pages).toBe(pages); + }); + + it('lists the six chapters the document actually contains', () => { + // Not machine-checkable: the text lives in compressed streams. Verify by + // hand with the command in the module's docblock when the PDF changes. + expect(FIELD_REPORT.chapters).toEqual([ + 'Streaming State Management', + 'Thread Persistence', + 'Tool-Call Rendering', + 'Human Approval Flows', + 'Generative UI', + 'Deterministic Testing', + ]); + }); + + it('carries the cover strings the document prints', () => { + expect(FIELD_REPORT.title).toBe('From Prototype to Production'); + expect(FIELD_REPORT.kicker).toBe('Threadplane · Open source · Angular'); + expect(FIELD_REPORT.subtitle).toBeTruthy(); + expect(FIELD_REPORT.year).toBe('2026'); + }); +}); diff --git a/apps/website/src/lib/field-report.ts b/apps/website/src/lib/field-report.ts new file mode 100644 index 000000000..de00ef6ff --- /dev/null +++ b/apps/website/src/lib/field-report.ts @@ -0,0 +1,35 @@ +/** + * The field report's own facts, in one place. + * + * These exist as data because the site got them wrong: it advertised "18 + * pages" for a 17-page document, and described the contents as "Error + * boundaries, fallbacks, observability, deploy" — three phrases that return + * zero matches in the file. Anything the site says about the document now + * reads from here, and `field-report.spec.ts` pins the page count to the PDF + * itself. + * + * To re-verify after regenerating the PDF: + * + * pdftotext -f 1 -l 2 apps/website/public/whitepaper.pdf - | sed -n '1,40p' + */ +export const FIELD_REPORT = { + /** Printed on the cover. */ + title: 'From Prototype to Production', + /** The cover's header rule. */ + kicker: 'Threadplane · Open source · Angular', + /** The cover's standfirst, verbatim. */ + subtitle: 'Production-ready chat, threads, and generative UI for AI agents.', + /** The cover's date line. */ + year: '2026', + /** Verified against the file, and asserted against it in the spec. */ + pages: 17, + /** The table of contents, in document order. */ + chapters: [ + 'Streaming State Management', + 'Thread Persistence', + 'Tool-Call Rendering', + 'Human Approval Flows', + 'Generative UI', + 'Deterministic Testing', + ], +} as const; From 38b5962d5591bd1421f36f3b0994e8ed5d5b72cb Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:34:20 -0700 Subject: [PATCH 04/10] docs: correct the plan's PDF path guidance readFileSync anchors to the repo root, not the vitest root, and jsdom makes import.meta.url an http: URL. Both simpler forms throw. Co-Authored-By: Claude Opus 5 --- docs/superpowers/plans/2026-09-08-teams-briefing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/superpowers/plans/2026-09-08-teams-briefing.md b/docs/superpowers/plans/2026-09-08-teams-briefing.md index 725ff4051..84c52e7bb 100644 --- a/docs/superpowers/plans/2026-09-08-teams-briefing.md +++ b/docs/superpowers/plans/2026-09-08-teams-briefing.md @@ -98,7 +98,7 @@ describe('FIELD_REPORT', () => { }); ``` -Note the path is `public/whitepaper.pdf`, relative to the vitest root (`apps/website`), not the repo root. +**Correction, found during execution:** the path above does not resolve. `readFileSync` anchors to `process.cwd()`, which is the **repo root** under both `nx test` and `vitest --root apps/website`; and `import.meta.url` is no help either, because jsdom makes it an `http:` URL. Anchor to the workspace root by walking up to `nx.json`, the way `cockpit-docs-links.spec.ts` in the same directory already does. The shipped spec has the working version. - [ ] **Step 2: Run it and watch it fail** From 773fff9e09efc82a85bfb6c24b322de27bd11d93 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:37:23 -0700 Subject: [PATCH 05/10] feat(website): the briefing preview shows the real contents Co-Authored-By: Claude Opus 5 --- .../landing/FieldReportCover.spec.tsx | 41 +++++++++++++++++++ .../components/landing/FieldReportCover.tsx | 41 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 apps/website/src/components/landing/FieldReportCover.spec.tsx create mode 100644 apps/website/src/components/landing/FieldReportCover.tsx diff --git a/apps/website/src/components/landing/FieldReportCover.spec.tsx b/apps/website/src/components/landing/FieldReportCover.spec.tsx new file mode 100644 index 000000000..105013109 --- /dev/null +++ b/apps/website/src/components/landing/FieldReportCover.spec.tsx @@ -0,0 +1,41 @@ +// @vitest-environment jsdom +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { FieldReportCover } from './FieldReportCover'; +import { FIELD_REPORT } from '../../lib/field-report'; + +describe('FieldReportCover', () => { + it('shows every chapter, in order', () => { + const { container } = render(); + const items = Array.from(container.querySelectorAll('.field-report-toc li')); + expect(items.map((li) => li.textContent?.replace(/^\d+/, '').trim())).toEqual([ + ...FIELD_REPORT.chapters, + ]); + }); + + it('is readable rather than decorative', () => { + // The old .wp-cover-wrap is aria-hidden because it is artwork. This one + // carries the table of contents, which is the reason to download — hiding + // it would withhold the substance from screen reader users. + const { container } = render(); + expect(container.querySelector('[aria-hidden="true"].field-report-paper')).toBeNull(); + expect(screen.getByRole('heading', { level: 3 }).textContent).toBe(FIELD_REPORT.title); + expect(container.querySelector('ol.field-report-toc')).toBeTruthy(); + }); + + it('does not read the numbers twice', () => { + // The
        already numbers the list for assistive tech; the drawn 01/02 + // markers are visual duplicates and must be hidden. + const { container } = render(); + const nums = Array.from(container.querySelectorAll('.field-report-num')); + expect(nums).toHaveLength(FIELD_REPORT.chapters.length); + for (const n of nums) expect(n.getAttribute('aria-hidden')).toBe('true'); + }); + + it('borrows the library pages’ paper styling', () => { + // Same object the four library pages already show, so it is not a second + // visual language for the same artifact. + const { container } = render(); + expect(container.querySelector('.wp-paper.field-report-paper')).toBeTruthy(); + }); +}); diff --git a/apps/website/src/components/landing/FieldReportCover.tsx b/apps/website/src/components/landing/FieldReportCover.tsx new file mode 100644 index 000000000..2fa5392fd --- /dev/null +++ b/apps/website/src/components/landing/FieldReportCover.tsx @@ -0,0 +1,41 @@ +import { FIELD_REPORT } from '../../lib/field-report'; + +/** + * The field report rendered as the object it is, contents included. + * + * It reuses `.wp-paper` — the tilted card the library pages already show — + * so the same artifact does not get two visual languages, and adds its own + * classes for the content the library version does not have. + * + * Deliberately NOT aria-hidden. The library page's cover is artwork and hides + * itself; this one prints the table of contents, which is the reason anyone + * gives up an email address for it. + */ +export function FieldReportCover() { + return ( +
        +
        +
        +

        {FIELD_REPORT.kicker}

        +

        {FIELD_REPORT.title}

        +

        {FIELD_REPORT.subtitle}

        +

        Contents

        +
          + {FIELD_REPORT.chapters.map((chapter, i) => ( +
        1. + + {chapter} +
        2. + ))} +
        +
        +

        + threadplane.ai + {FIELD_REPORT.year} +

        +
        +
        + ); +} From 3ccaa354c569c90760df2dad05f86ec9b047c1d8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:40:10 -0700 Subject: [PATCH 06/10] feat(website): For teams leads with the briefing, not the pilot timeline Co-Authored-By: Claude Opus 5 --- apps/website/e2e/website.spec.ts | 2 +- .../components/landing/TeamsBlock.spec.tsx | 61 ++++++++-- .../src/components/landing/TeamsBlock.tsx | 115 ++++++++---------- 3 files changed, 101 insertions(+), 77 deletions(-) diff --git a/apps/website/e2e/website.spec.ts b/apps/website/e2e/website.spec.ts index 4216f0b2f..bb512e596 100644 --- a/apps/website/e2e/website.spec.ts +++ b/apps/website/e2e/website.spec.ts @@ -54,7 +54,7 @@ test('landing page renders the spine in order (live-stage spec §3)', async ({ p 'architecture-heading', 'stage-heading', 'open-source-heading', - 'pilot-heading', + 'field-report-heading', 'faq-heading', ]; const tops: number[] = []; diff --git a/apps/website/src/components/landing/TeamsBlock.spec.tsx b/apps/website/src/components/landing/TeamsBlock.spec.tsx index 44fa93846..616de39d2 100644 --- a/apps/website/src/components/landing/TeamsBlock.spec.tsx +++ b/apps/website/src/components/landing/TeamsBlock.spec.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { TeamsBlock } from './TeamsBlock'; +import { FIELD_REPORT } from '../../lib/field-report'; vi.mock('../../lib/analytics/client', () => ({ track: vi.fn(), @@ -16,22 +17,58 @@ const formPolicy = { } as never; describe('TeamsBlock', () => { - it('renders the pilot heading, four outcomes, four phases, both CTAs, and one email form', () => { + it('leads with the ask: eyebrow, heading, then the form', () => { const { container } = render(); - expect(screen.getByRole('heading', { name: 'Shipping inside a large Angular platform?' }).id).toBe('pilot-heading'); - expect(container.querySelectorAll('.pilot-row')).toHaveLength(4); - expect(container.querySelectorAll('.pilot-step')).toHaveLength(4); - expect(screen.getByRole('link', { name: 'Talk to an engineer' }).getAttribute('href')).toBe('/contact?source=home_enterprise&track=enterprise'); - expect(screen.getByRole('link', { name: 'See the pilot program' }).getAttribute('href')).toBe('/pilot-to-prod'); + const heading = screen.getByRole('heading', { level: 2 }); + expect(heading.textContent).toBe('What breaks between a demo and production.'); + expect(heading.id).toBe('field-report-heading'); expect(container.querySelectorAll('form')).toHaveLength(1); expect(screen.getByLabelText('Work email')).toBeTruthy(); - expect(screen.getByText('Whitepaper disclosure')).toBeTruthy(); + expect(screen.getByText(formPolicy.disclosures.whitepaper)).toBeTruthy(); }); - it('frames the field report as the takeaway, not a second section', () => { - render(); - expect(screen.getByText('Field report')).toBeTruthy(); - expect(screen.getByText('From Prototype to Production')).toBeTruthy(); - expect(screen.queryByRole('heading', { name: 'The last-mile gap in Angular AI.' })).toBeNull(); + it('derives the page count rather than typing it', () => { + // The section claimed "18 pages" for a 17-page document. Reading it from + // FIELD_REPORT means the guard against the PDF covers this string too. + const { container } = render(); + const eyebrow = container.querySelector('[data-ui="eyebrow"]'); + expect(eyebrow?.textContent).toContain(`${FIELD_REPORT.pages} pages`); + expect(eyebrow?.textContent).toContain('Preflight briefing'); + }); + + it('shows the briefing beside the ask', () => { + const { container } = render(); + expect(container.querySelector('.field-report-paper')).toBeTruthy(); + }); + + it('makes contact the secondary ask, not a rival button', () => { + // Two same-weight buttons is what stopped the old section saying which + // ask mattered. The arrow is decorative so the accessible name stays clean. + const { container } = render(); + const link = screen.getByRole('link', { name: 'Talk to an engineer' }); + expect(link.getAttribute('href')).toBe('/contact?source=home_enterprise&track=enterprise'); + expect(link.getAttribute('data-ui')).not.toBe('button'); + // Exactly one button in the whole section, and it belongs to the form. + // (SubmitButton wraps Button, so the form's submit carries data-ui too.) + const buttons = container.querySelectorAll('[data-ui="button"]'); + expect(buttons).toHaveLength(1); + expect(buttons[0].closest('form')).toBeTruthy(); + }); + + it('no longer repeats the pilot programme the dedicated page owns', () => { + const { container } = render(); + expect(container.querySelectorAll('.pilot-step')).toHaveLength(0); + expect(container.querySelectorAll('.pilot-row')).toHaveLength(0); + expect(screen.queryByRole('link', { name: 'See the pilot program' })).toBeNull(); + }); + + it('never advertises topics the document does not contain', () => { + // "Error boundaries", "fallbacks" and "observability" each return zero + // matches in whitepaper.pdf. They were on this page for months. + const { container } = render(); + const text = container.textContent ?? ''; + for (const phrase of ['Error boundaries', 'fallbacks', 'observability', '18 pages']) { + expect(text).not.toContain(phrase); + } }); }); diff --git a/apps/website/src/components/landing/TeamsBlock.tsx b/apps/website/src/components/landing/TeamsBlock.tsx index 4e14a5225..50e3101d6 100644 --- a/apps/website/src/components/landing/TeamsBlock.tsx +++ b/apps/website/src/components/landing/TeamsBlock.tsx @@ -4,81 +4,68 @@ import type { PublicFormPolicy } from '../../lib/growth/form-policy'; import { Container } from '../ui/Container'; import { Section } from '../ui/Section'; import { Eyebrow } from '../ui/Eyebrow'; -import { Button } from '../ui/Button'; import { trackCtaClick } from '../../lib/analytics/client'; import { WhitePaperForm } from './WhitePaperForm'; - -const TIMELINE = [ - { phase: '01', title: 'Discover', body: 'Map your stack, surfaces, and the agentic work that earns its keep.' }, - { phase: '02', title: 'Build', body: 'A working demo on your real data, in your real app.' }, - { phase: '03', title: 'Harden', body: 'Observability, error boundaries, deploy paths, on-call patterns.' }, - { phase: '04', title: 'Train', body: 'Your team owns the stack. We leave you with a runbook, not a black box.' }, -]; - -const OUTCOMES = [ - { claim: 'A working agent demo on your domain', tail: 'your data' }, - { claim: 'Hardened error, fallback, observability patterns', tail: 'production-ready' }, - { claim: 'Deploy-ready integration', tail: 'your CI/CD' }, - { claim: 'Team trained on the framework', tail: 'runbook, yours' }, -]; +import { FieldReportCover } from './FieldReportCover'; +import { FIELD_REPORT } from '../../lib/field-report'; /** - * For teams (live-stage spec §3, block 6): the pilot program and the field - * report in one block, with the page's one email form. Replaces PilotBlock + - * WhitePaperBlock on the homepage; the library pages keep WhitePaperBlock. + * For teams: the field report is the ask, contact is the follow-up. + * + * The previous version inverted its own goals — the form sat at y=618 in a + * 916px section, below a four-step pilot timeline that `/pilot-to-prod` + * already covers in full, while a same-weight amber button sent people to + * contact instead. The timeline and the outcome rows are gone, the form sits + * beside a preview of the actual document, and contact is a link. + * + * Everything the section says about the report reads from FIELD_REPORT, which + * is pinned to the PDF — this block previously advertised a page count and a + * contents list that the file did not match. */ export function TeamsBlock({ formPolicy }: { formPolicy: PublicFormPolicy }) { return ( -
        +
        -
        +
        -
        - For teams -
        -

        Shipping inside a large Angular platform?

        -

        - Bring your backend, security model, and design system. Work directly with Threadplane - engineers on architecture, rollout, testing, and production hardening. -

        -
        - {OUTCOMES.map((o) => ( -
        -

        {o.claim}

        -

        {o.tail}

        -
        - ))} -
        -
        - - -
        + + Preflight briefing + + {' '}· {FIELD_REPORT.pages} pages · free + + +

        + What breaks between a demo and production. +

        +
        + +
        -
        -
        - {TIMELINE.map((t) => ( -
        - -
        -
        {t.title}
        -
        {t.body}
        -
        -
        - ))} -
        -
        - Field report -
        18 pages · free
        -

        From Prototype to Production

        -

        Six production-readiness dimensions for Angular AI teams. Error boundaries, fallbacks, observability, deploy. Free.

        - -
        -
        +
        +

        + Shipping inside a large Angular platform? Bring your backend, security + model, and design system. +

        + + trackCtaClick({ + cta_id: 'hero_talk_to_engineers', + track: 'enterprise', + surface: 'home', + }) + } + > + Talk to an engineer +
        From eceabafcd39e3ef1b79b1e20883f9918044a94b2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 8 Sep 2026 16:42:25 -0700 Subject: [PATCH 07/10] fix(website): the library pages described a document we do not send Co-Authored-By: Claude Opus 5 --- .../website/src/components/landing/WhitePaperBlock.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/website/src/components/landing/WhitePaperBlock.tsx b/apps/website/src/components/landing/WhitePaperBlock.tsx index 4502d5e13..be6922196 100644 --- a/apps/website/src/components/landing/WhitePaperBlock.tsx +++ b/apps/website/src/components/landing/WhitePaperBlock.tsx @@ -3,10 +3,14 @@ import { Container } from '../ui/Container'; import { Section } from '../ui/Section'; import { Eyebrow } from '../ui/Eyebrow'; import { WhitePaperForm, type WhitepaperId } from './WhitePaperForm'; +import { FIELD_REPORT } from '../../lib/field-report'; const ROWS = [ - { claim: 'Six production-readiness dimensions', tail: '18 pages' }, - { claim: 'Error boundaries, fallbacks, observability, deploy', tail: 'concrete patterns' }, + { claim: 'Six production-readiness dimensions', tail: `${FIELD_REPORT.pages} pages` }, + // Was "Error boundaries, fallbacks, observability, deploy" — three phrases + // with zero matches in whitepaper.pdf. These are the document's actual + // chapters. + { claim: 'Streaming, persistence, tool calls, approvals, generative UI, testing', tail: 'the six chapters' }, { claim: 'No vendor pitch — what we learned shipping it', tail: 'free' }, ]; @@ -54,7 +58,7 @@ export function WhitePaperBlock({