From ce5309f6a33b449b9629844028aeb16b31f9b9ab Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 4 Jun 2026 10:33:02 -0700 Subject: [PATCH 1/8] docs(spec): docs chrome polish (sidebar marks, page header, prev/next) Carry the landing's visual language into the docs reading chrome via a shared LibraryMark: sidebar switcher marks + nav cleanup (background-only active/hover, no left bar), a branded page header with a reserved actions slot, and hover-lift prev/next cards. First of two specs; the LLM markdown export lands separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-04-docs-chrome-polish-design.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-04-docs-chrome-polish-design.md diff --git a/docs/superpowers/specs/2026-06-04-docs-chrome-polish-design.md b/docs/superpowers/specs/2026-06-04-docs-chrome-polish-design.md new file mode 100644 index 00000000..01c18f66 --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-docs-chrome-polish-design.md @@ -0,0 +1,123 @@ +# Docs Chrome Polish — Design + +**Date:** 2026-06-04 +**Status:** Draft for review +**Scope:** Carry the landing-page visual language into the docs reading chrome — the sidebar, the per-page header, and the prev/next footer. Frontend/visual only. Builds on the branded landing shipped in #564/#568. + +## Goal + +The `/docs` landing page is now branded (vendor/library marks, accent treatment, +hover-lift cards). The inner docs pages still use plainer styling. This spec +extends the same language to the docs chrome so the reading experience matches +the landing. + +This is the **first of two** related specs. A later spec adds Mintlify-style +per-page LLM markdown export (a raw-markdown route + a `PageActions` dropdown); +this spec reserves the slot where that control mounts but does not build it. + +## Shared foundation — `LibraryMark` + +A single small component that maps each of the seven libraries to its visual +mark, reusing the exact assets/glyphs from the landing page: + +| Library | Mark | +|---------|------| +| langgraph | `/logos/langgraph.svg` (logo chip) | +| ag-ui | `/logos/runtimes/copilotkit.svg` (logo chip) | +| a2ui | `/logos/providers/google.svg` (logo chip) | +| render | `/logos/surface/vercel.svg` (logo chip) | +| chat | in-house speech-bubble glyph | +| licensing | in-house key glyph | +| telemetry | in-house pulse glyph | + +- **Create:** `apps/website/src/components/docs/LibraryMark.tsx`. +- Props: `{ library: LibraryId; size?: number }` (default size 24). +- Logo libraries render the white rounded "logo chip" treatment (white bg, + border, contained ``); glyph libraries + render the accent-tinted "glyph chip" (accent-surface bg, accent border, the + stroked SVG in `tokens.colors.accent`). Same visual styling as the landing's + `LogoChip`/`GlyphChip`. +- The 3 glyph SVGs (speech-bubble, key, pulse) move here as the single source of + truth. (The landing page keeps its own copies — out of scope to refactor the + already-merged landing in this spec.) +- Server-component safe (no client hooks). + +## 1. Sidebar library switcher + +In `apps/website/src/components/docs/DocsSidebar.tsx`: +- The current-library button (top of `LibraryDropdown`) renders a small + `LibraryMark` (size ~20) left of the library name. +- Each dropdown option renders its `LibraryMark` left of the title/description. +- Behavior, routing, and the open/close logic are unchanged. + +## 2. Sidebar section hierarchy (nav cleanup) + +In `DocsSidebar.tsx`, `SectionGroup` page links: +- **Active page:** accent-surface background only. **Remove any left accent bar** + — background is the only active affordance. Text stays `tokens.colors.accent`, + weight 600. +- **Hover (inactive):** a subtle background (`tokens.surfaces.surfaceDim`) via a + scoped ` +``` + +- [ ] **Step 6: Lint + commit** + +```bash +cd /Users/blove/repos/angular-agent-framework +npx eslint apps/website/src/components/docs/DocsSidebar.tsx +git add apps/website/src/components/docs/DocsSidebar.tsx +git commit -m "feat(website): docs sidebar library marks + nav hover state" +``` +Expected: eslint exit 0. + +--- + +## Task 4: Prev/Next hover-lift cards + +**Files:** +- Modify: `apps/website/src/components/docs/DocsPrevNext.tsx` + +Context: the component already renders `Card` + `Eyebrow` direction labels. This task swaps the generic `hoverable` for the landing's `data-ui="docs-card"` lift (border → `accentBorderHover`, shadow → `md`, `translateY(-1px)`, reduced-motion guarded) and makes the page titles accent-colored so the cards read as links. + +- [ ] **Step 1: Replace the component body** + +Replace the entire `return (...)` of `DocsPrevNext` (the `` block, lines 47-97) with: + +```tsx + return ( + + ); +``` + +- [ ] **Step 2: Lint + commit** + +```bash +cd /Users/blove/repos/angular-agent-framework +npx eslint apps/website/src/components/docs/DocsPrevNext.tsx +git add apps/website/src/components/docs/DocsPrevNext.tsx +git commit -m "feat(website): docs prev/next hover-lift cards" +``` +Expected: eslint exit 0. + +--- + +## Task 5: e2e assertions + verify + +**Files:** +- Modify: `apps/website/e2e/docs.spec.ts` + +- [ ] **Step 1: Add chrome assertions to the slug-page test** + +In `apps/website/e2e/docs.spec.ts`, find the test `'renders breadcrumb + h1 + sidebar'` inside the `Docs slug page` describe block. Replace that single test with: + +```ts + test('renders breadcrumb + h1 + sidebar', async ({ page }) => { + await page.goto(route); + await expect(page.locator('aside').first()).toBeVisible(); + await expect(page.locator('nav[aria-label="Breadcrumb"]').first()).toBeVisible(); + await expect(page.locator('article').first()).toBeVisible(); + }); + + test('renders the branded chrome (sidebar mark, page-header eyebrow, prev/next direction)', async ({ page }) => { + await page.goto(route); + // Sidebar shows the active library's logo mark + await expect(page.locator('aside img[src="/logos/langgraph.svg"]').first()).toBeVisible(); + // Branded page header eyebrow + await expect(page.getByText(/LangGraph\s+·\s+Getting Started/i).first()).toBeVisible(); + // Prev/Next: introduction is the first page, so a "Next →" card is present + await expect(page.getByText('Next →').first()).toBeVisible(); + }); +``` + +(`route` is the existing `const route = '/docs/langgraph/getting-started/introduction';` already declared in that describe block.) + +- [ ] **Step 2: Run the docs e2e file** + +Run: +```bash +cd apps/website && npx playwright test e2e/docs.spec.ts +``` +Expected: PASS — all blocks (landing, slug page incl. the two tests above, search). The dev server auto-starts via `playwright.config.ts`. + +- [ ] **Step 3: Commit** + +```bash +cd /Users/blove/repos/angular-agent-framework +git add apps/website/e2e/docs.spec.ts +git commit -m "test(website): assert branded docs chrome (mark, header eyebrow, prev/next)" +``` + +--- + +## Manual verification (browser) + +After e2e passes, open `/docs/langgraph/guides/streaming` on the dev server and confirm: + +- [ ] Sidebar library switcher shows the LangGraph mark; opening the dropdown shows a mark beside each library. +- [ ] Active section link has an accent background (no left bar); hovering an inactive link shows a subtle background. +- [ ] The page header shows the mark + `LANGGRAPH · GUIDES` eyebrow above the article title (the MDX `# Streaming` h1 is not duplicated). +- [ ] Prev/Next render as cards that lift on hover, with `← PREVIOUS` / `NEXT →` labels. +- [ ] No console errors; switch a couple of libraries (e.g. `/docs/chat/...`, `/docs/telemetry/...`) and confirm glyph chips render for the in-house libraries. + +--- + +## Self-Review (completed during planning) + +- **Spec coverage:** `LibraryMark` with all 7 mappings + glyphs ✓ (Task 1). Sidebar switcher marks ✓ (Task 3 steps 2-3). Nav cleanup — background-only active (already true, preserved) + hover background ✓ (Task 3 steps 4-5). Branded page header with mark + `LIBRARY · SECTION` eyebrow + reserved `actions` slot, MDX h1 preserved ✓ (Task 2). Prev/Next hover-lift cards with direction labels ✓ (Task 4). Tests: `LibraryMark.spec.tsx` ✓, e2e chrome assertions ✓ (Task 5). Actions slot reserved but unused (Spec 2) ✓. No spec requirement unimplemented. +- **Placeholder scan:** No TBD/TODO; every code step shows complete code; commands have expected output. +- **Type consistency:** `LibraryMark` props `{ library, size }` used identically in sidebar (size 20) and header (size 34). `MARKS` is `Record` (compiler enforces all 7 keys). `DocsPageHeader` props `{ library, section, actions }` match the route call (no `actions` passed). `getDocsSection`/`getLibraryConfig` are existing exports of `docs-config`. `GlyphKey` union (`chat`/`key`/`pulse`) matches the `GLYPHS` record and the `glyph` fields in `MARKS`. +``` From 1261dedf80b20a74d0531563929c26405984d754 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 4 Jun 2026 14:17:26 -0700 Subject: [PATCH 3/8] feat(website): add LibraryMark component for docs chrome Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/docs/LibraryMark.spec.tsx | 29 +++++ .../src/components/docs/LibraryMark.tsx | 104 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 apps/website/src/components/docs/LibraryMark.spec.tsx create mode 100644 apps/website/src/components/docs/LibraryMark.tsx diff --git a/apps/website/src/components/docs/LibraryMark.spec.tsx b/apps/website/src/components/docs/LibraryMark.spec.tsx new file mode 100644 index 00000000..cff52a09 --- /dev/null +++ b/apps/website/src/components/docs/LibraryMark.spec.tsx @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// @vitest-environment jsdom +import React from 'react'; +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { LibraryMark } from './LibraryMark'; + +describe('LibraryMark', () => { + it('renders a logo image for a logo-backed library', () => { + const { container } = render(); + const img = container.querySelector('img'); + expect(img).not.toBeNull(); + expect(img?.getAttribute('src')).toBe('/logos/langgraph.svg'); + expect(img?.getAttribute('alt')).toBe(''); + }); + + it('renders an inline glyph (svg, no img) for an in-house library', () => { + const { container } = render(); + expect(container.querySelector('img')).toBeNull(); + expect(container.querySelector('svg')).not.toBeNull(); + }); + + it('maps ag-ui and a2ui to their vendor logos', () => { + const { container: a } = render(); + expect(a.querySelector('img')?.getAttribute('src')).toBe('/logos/runtimes/copilotkit.svg'); + const { container: b } = render(); + expect(b.querySelector('img')?.getAttribute('src')).toBe('/logos/providers/google.svg'); + }); +}); diff --git a/apps/website/src/components/docs/LibraryMark.tsx b/apps/website/src/components/docs/LibraryMark.tsx new file mode 100644 index 00000000..480b055a --- /dev/null +++ b/apps/website/src/components/docs/LibraryMark.tsx @@ -0,0 +1,104 @@ +import { tokens } from '@threadplane/design-tokens'; +import type { LibraryId } from '../../lib/docs-config'; + +type GlyphKey = 'chat' | 'key' | 'pulse'; + +type MarkEntry = + | { kind: 'logo'; src: string } + | { kind: 'glyph'; glyph: GlyphKey }; + +const MARKS: Record = { + langgraph: { kind: 'logo', src: '/logos/langgraph.svg' }, + 'ag-ui': { kind: 'logo', src: '/logos/runtimes/copilotkit.svg' }, + a2ui: { kind: 'logo', src: '/logos/providers/google.svg' }, + render: { kind: 'logo', src: '/logos/surface/vercel.svg' }, + chat: { kind: 'glyph', glyph: 'chat' }, + licensing: { kind: 'glyph', glyph: 'key' }, + telemetry: { kind: 'glyph', glyph: 'pulse' }, +}; + +function ChatGlyph({ s }: { s: number }) { + return ( + + ); +} + +function KeyGlyph({ s }: { s: number }) { + return ( + + ); +} + +function PulseGlyph({ s }: { s: number }) { + return ( + + ); +} + +const GLYPHS: Record React.JSX.Element> = { + chat: ChatGlyph, + key: KeyGlyph, + pulse: PulseGlyph, +}; + +interface Props { + library: LibraryId; + /** Outer chip size in px. Default 24. */ + size?: number; +} + +export function LibraryMark({ library, size = 24 }: Props) { + const mark = MARKS[library]; + const base = { + width: size, + height: size, + borderRadius: tokens.radius.md, + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flex: '0 0 auto', + } as const; + + if (mark.kind === 'logo') { + const inner = Math.round(size * 0.6); + return ( + + + + ); + } + + const Glyph = GLYPHS[mark.glyph]; + return ( + + + + ); +} From a5bbeec101612871c1a0bd341f743eccece4c26e Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 4 Jun 2026 14:51:06 -0700 Subject: [PATCH 4/8] feat(website): branded docs page header with reserved actions slot --- .../docs/[library]/[section]/[slug]/page.tsx | 2 + .../src/components/docs/DocsPageHeader.tsx | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 apps/website/src/components/docs/DocsPageHeader.tsx diff --git a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx index af130c5b..42aa5c15 100644 --- a/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx +++ b/apps/website/src/app/docs/[library]/[section]/[slug]/page.tsx @@ -5,6 +5,7 @@ import { DocsSidebar } from '../../../../../components/docs/DocsSidebar'; import { MdxRenderer } from '../../../../../components/docs/MdxRenderer'; import { DocsSearch } from '../../../../../components/docs/DocsSearch'; import { DocsBreadcrumb } from '../../../../../components/docs/DocsBreadcrumb'; +import { DocsPageHeader } from '../../../../../components/docs/DocsPageHeader'; import { DocsPrevNext } from '../../../../../components/docs/DocsPrevNext'; import { getDocBySlug, getAllDocSlugs, getDocMetadata } from '../../../../../lib/docs'; import { ApiDocRenderer, type ApiDocEntry } from '../../../../../components/docs/ApiDocRenderer'; @@ -73,6 +74,7 @@ export default async function DocsPage({ params }: DocsRouteProps) {
+
c.toUpperCase()); +} + +interface Props { + library: LibraryId; + section: string; + /** Right-aligned slot for per-page actions (Spec 2). Optional. */ + actions?: ReactNode; +} + +export function DocsPageHeader({ library, section, actions }: Props) { + const libTitle = getLibraryConfig(library)?.title ?? library; + const sectionTitle = getDocsSection(library, section)?.title ?? humanize(section); + + return ( +
+
+ + + {libTitle} · {sectionTitle} + +
+ {actions ?
{actions}
: null} +
+ ); +} From 0ca22b0f61fd31a005b513a1368e2d267337abc6 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 4 Jun 2026 14:53:44 -0700 Subject: [PATCH 5/8] feat(website): docs sidebar library marks + nav hover state --- .../src/components/docs/DocsSidebar.tsx | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/apps/website/src/components/docs/DocsSidebar.tsx b/apps/website/src/components/docs/DocsSidebar.tsx index 05f94393..b99405ee 100644 --- a/apps/website/src/components/docs/DocsSidebar.tsx +++ b/apps/website/src/components/docs/DocsSidebar.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { docsConfig, getLibraryConfig, type DocsSection, type LibraryId } from '../../lib/docs-config'; import { tokens } from '@threadplane/design-tokens'; import { Pill } from '../ui/Pill'; +import { LibraryMark } from './LibraryMark'; interface Props { activeLibrary: LibraryId; @@ -40,8 +41,11 @@ function LibraryDropdown({ activeLibrary }: { activeLibrary: LibraryId }) { fontWeight: 600, }} > - - {currentLib?.title ?? activeLibrary} + + + + {currentLib?.title ?? activeLibrary} + - - {lib.title} + + - - {lib.description} + + + {lib.title} + + + {lib.description} + ))} @@ -146,6 +155,8 @@ function SectionGroup({ + {/* Search trigger */}