Skip to content

Commit 2cbd506

Browse files
bloveclaude
andcommitted
refactor(website): finish the json-render rename; generalise style contracts
Two follow-ups from #923. json-render on marketing surfaces --------------------------------- #923 renamed the docs picker label but deliberately left the marketing surfaces. They now agree: the homepage FeatureBlock eyebrow, the footer link, and the three solutions architecture layers all read json-render. Two hazards this surfaced: - The footer derives cta_id from the visible label, so renaming the link would have silently split PostHog's footer_render into a new footer_json_render series. trackFooterCta now takes an optional explicit CtaId and the render link pins the original. - The solutions page maps library name to href through a Record<string, string>. Renaming the data without the key returns undefined and renders the card unlinked — no error, no type failure. solutions-links.spec.ts asserts every layer resolves; mutation-tested by renaming one side only. Style contracts --------------- docs-sidebar-styles.spec.ts guarded two CSS declarations whose loss is invisible to jsdom, but it was a one-off with an inline parser. It is replaced by style-contract.ts plus a registry in style-contracts.spec.ts, so adding a guard is one entry rather than a new file. Seeded with four live rules, each mutation-tested by deleting the declaration and by renaming the selector wholesale: - .docs-sidebar-lib-item-text flex column (the #892 collision) - .docs-sidebar-lib-menu max-height + overflow-y - .docs-control-plane position sticky + align-self - [data-control-plane-pane] overflow-y Writing the registry found a bug in the extracted parser: a CSS comment above a rule lands inside the selector capture, so the exact match never fires and a guarded rule reports as missing. Comments are stripped first. Also removes the DocsSidebar wrapper and its .docs-sidebar rule, dead since #892 moved the docs nav into the control plane — only DocsNavigation is imported, and the sole class usage was inside the dead component. Its comment documented the same align-self hazard now held by the .docs-control-plane contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 69c2d54 commit 2cbd506

11 files changed

Lines changed: 171 additions & 73 deletions

File tree

apps/website/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default async function HomePage() {
6767
{/* Render */}
6868
<FeatureBlock
6969
id="render"
70-
eyebrow="Render"
70+
eyebrow="json-render"
7171
headline="Agent output, rendered as your components."
7272
body="The server emits a JSON spec. Angular renders it with components you own — json-render and A2UI both speak it."
7373
rows={[

apps/website/src/app/solutions/[slug]/page.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ interface PageProps {
2222
params: Promise<{ slug: string }>;
2323
}
2424

25-
const LIBRARY_HREF: Record<string, string> = {
25+
/**
26+
* Keyed by the display name in solutions-data. A miss renders the card
27+
* unlinked rather than failing, and `Record<string, string>` will not catch a
28+
* rename on either side — so `solutions-links.spec.ts` asserts every layer
29+
* resolves.
30+
*/
31+
export const LIBRARY_HREF: Record<string, string> = {
2632
Agent: '/langgraph',
27-
Render: '/render',
33+
'json-render': '/render',
2834
Chat: '/chat',
2935
};
3036

apps/website/src/components/docs/DocsSidebar.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,3 @@ export function DocsNavigation({
397397
</div>
398398
);
399399
}
400-
401-
export function DocsSidebar(props: DocsNavigationProps) {
402-
return (
403-
<aside className="docs-sidebar">
404-
<DocsNavigation {...props} />
405-
</aside>
406-
);
407-
}

apps/website/src/components/landing/FeatureBlock.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { describe, it, expect } from 'vitest';
33
import { FeatureBlock } from './FeatureBlock';
44

55
const base = {
6-
eyebrow: 'Render',
6+
eyebrow: 'json-render',
77
headline: 'Agent output, rendered as your components.',
88
body: 'Two sentences.',
99
cta: { label: 'See it', href: '/render' },

apps/website/src/components/shared/Footer.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { useState } from 'react';
33
import Link from 'next/link';
4-
import { analyticsEvents } from '../../lib/analytics/events';
4+
import { analyticsEvents, type CtaId } from '../../lib/analytics/events';
55
import { track, trackCtaClick, trackExternalLinkClick } from '../../lib/analytics/client';
66
import { DEMOS, demoCtaSuffix } from '../../lib/demos';
77
import { LogoMark } from '../ui/LogoMark';
@@ -90,11 +90,18 @@ function NewsletterForm() {
9090
}
9191

9292
export function Footer() {
93-
const trackFooterCta = (label: string, href: string) => {
93+
/**
94+
* `ctaId` defaults to a slug of the label. Pass it explicitly when the visible
95+
* text changes but the analytics series should stay continuous — renaming
96+
* "Render" to "json-render" would otherwise silently split footer_render into
97+
* a new footer_json_render series.
98+
*/
99+
const trackFooterCta = (label: string, href: string, ctaId?: CtaId) => {
94100
trackCtaClick({
95101
surface: 'footer',
96102
destination_url: href,
97-
cta_id: `footer_${label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}`,
103+
cta_id:
104+
ctaId ?? `footer_${label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '')}`,
98105
cta_text: label,
99106
});
100107
};
@@ -200,8 +207,8 @@ export function Footer() {
200207
AG-UI
201208
</Link>
202209
<Link href="/render" className="transition-colors footer-link"
203-
onClick={() => trackFooterCta('Render', '/render')}>
204-
Render
210+
onClick={() => trackFooterCta('json-render', '/render', 'footer_render')}>
211+
json-render
205212
</Link>
206213
<Link href="/chat" className="transition-colors footer-link"
207214
onClick={() => trackFooterCta('Chat', '/chat')}>

apps/website/src/lib/solutions-data.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const SOLUTIONS: SolutionConfig[] = [
124124
role: 'Production agent state with first-class interrupt support. Every agent action can require human approval before execution. Durable thread persistence preserves the full record of every tool call and state transition.',
125125
},
126126
{
127-
library: 'Render',
127+
library: 'json-render',
128128
pkg: '@threadplane/render',
129129
role: 'Approval workflows rendered as structured UI — not chat messages. The agent proposes an action, renders a confirmation card, and waits for the human gate before proceeding.',
130130
},
@@ -217,7 +217,7 @@ export const SOLUTIONS: SolutionConfig[] = [
217217
role: 'Streams query results token-by-token as the LangGraph agent reasons over your data. Thread persistence means users can refine questions without re-running expensive queries.',
218218
},
219219
{
220-
library: 'Render',
220+
library: 'json-render',
221221
pkg: '@threadplane/render',
222222
role: 'The agent emits chart specs, data tables, and KPI cards as structured render specs. Your Angular components render them with streaming JSON patches — live-updating visualizations as data arrives.',
223223
},
@@ -295,7 +295,7 @@ export class DashboardComponent {
295295
role: 'LangGraph interrupts let the agent pause before sensitive actions — refunds, account changes, escalations. Thread persistence preserves the full conversation across bot-to-human handoffs.',
296296
},
297297
{
298-
library: 'Render',
298+
library: 'json-render',
299299
pkg: '@threadplane/render',
300300
role: 'The agent renders structured UI — order summaries, refund confirmations, knowledge base cards — instead of dumping text. Customers see clean, actionable information.',
301301
},
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { LIBRARY_HREF } from '../app/solutions/[slug]/page';
3+
import { SOLUTIONS } from './solutions-data';
4+
5+
/**
6+
* Every architecture layer names a library, and the solutions page turns that
7+
* name into an href through a plain `Record<string, string>`. A miss is silent
8+
* — the card just renders without a link — and the types cannot catch a rename
9+
* on one side only.
10+
*/
11+
describe('solutions architecture layers', () => {
12+
it('every named library resolves to a href', () => {
13+
const unresolved = SOLUTIONS
14+
.flatMap((s) => s.architectureLayers.map((l) => l.library))
15+
.filter((library) => !LIBRARY_HREF[library]);
16+
17+
expect(unresolved).toEqual([]);
18+
});
19+
20+
it('names the render library the way the docs do', () => {
21+
const names = new Set(
22+
SOLUTIONS.flatMap((s) => s.architectureLayers.map((l) => l.library)),
23+
);
24+
25+
expect(names.has('json-render')).toBe(true);
26+
expect(names.has('Render')).toBe(false);
27+
});
28+
});

apps/website/src/styles/docs-sidebar-styles.spec.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

apps/website/src/styles/docs.css

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -729,17 +729,6 @@
729729
color: var(--color-text-primary);
730730
}
731731

732-
.docs-sidebar {
733-
border-right: 1px solid var(--color-border);
734-
background: var(--color-surface);
735-
position: sticky;
736-
top: var(--nav-h);
737-
/* Without align-self the flex row stretches the aside to the article's full
738-
* height (measured 10,030px), so its overflow-y:auto never engaged. */
739-
align-self: flex-start;
740-
min-height: calc(100vh - var(--nav-h));
741-
max-height: calc(100vh - var(--nav-h));
742-
}
743732
.docs-sidebar-lib-trigger {
744733
background: var(--color-surface);
745734
border: 1px solid var(--color-border);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { readFileSync } from 'node:fs';
2+
import { join } from 'node:path';
3+
4+
/**
5+
* Reading declarations out of a stylesheet, for rules whose loss is *silent*.
6+
*
7+
* jsdom does not apply stylesheets, so a component test renders the same DOM
8+
* whether or not a load-bearing declaration exists. That gap is how PR #892
9+
* shipped a docs picker whose title and description collided into one run-on
10+
* line: the JSX moved off Tailwind onto semantic class names, the
11+
* `flex flex-col` was never ported, and every test stayed green.
12+
*
13+
* Use this only for declarations where the failure mode is plausible-but-wrong
14+
* rendering. Ordinary styling belongs in review, not in a test.
15+
*
16+
* Limitation: this is a flat scan, not a CSS parser. Rules nested in
17+
* `@media` blocks are merged into the same selector's declarations, and
18+
* cascade order is not modelled. That is fine for asserting "this declaration
19+
* exists somewhere for this selector" and wrong for anything subtler.
20+
*/
21+
export function loadStylesheet(file: string): string {
22+
return readFileSync(join(__dirname, file), 'utf8');
23+
}
24+
25+
/** Merged declaration text for every rule whose selector list contains `selector`. */
26+
export function declarationsFor(css: string, selector: string): string {
27+
// Comments must go first: a `/* ... */` above a rule lands inside the
28+
// selector capture below, and the exact match then never fires. That reads
29+
// as "the rule is missing" — which is how a contract would report a false
30+
// failure the moment someone documented the rule it guards.
31+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
32+
33+
return [...withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)]
34+
.filter((match) => match[1].split(',').some((part) => part.trim() === selector))
35+
.map((match) => match[2])
36+
.join(';');
37+
}

0 commit comments

Comments
 (0)