Skip to content

Commit d89a681

Browse files
bloveclaude
andauthored
fix(docs): make the code header earn its space, and lead six pages with the example (#401)
* docs: title the fenced blocks that are really files 139 fenced code blocks across the docs carried no `title=`, so every one rendered a header bar blank except for its Copy button. Title the ten that name a file a reader would actually create, and leave the rest alone. The bar for inclusion is that the snippet is a complete module — its own imports, its own export — or that the prose already names the file. That covers the copy-this-into-your-project recipes (ThemeProvider.tsx, SystemThemeProvider.tsx, DensityPicker.tsx, useDensity.ts, useDensityHeights.ts), the three whole-grid recipes (People.tsx, EventGrid.tsx, OrdersGrid.tsx), the install block (Terminal), and the one fence whose page already says "`columns.ts` calls `createColumnHelper<Service>()`". Everything else stays untitled on purpose: type signatures, two-line `@import` pairs, JSX prop illustrations, and headless call sequences have no honest filename, and inventing one would teach a reader something false about where the code goes. Metadata only — no code inside any fence changed, and no prose reflowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: lead six pages with their running example Rule 1 of the examples-first spec is that the first substantial thing after the intro paragraph is an `<Example>`, not prose. These six pages opened with 4-16 lines of prose first, and `cell-presentations.mdx` was the worst of them: five paragraphs of abstract claims ("They are opt-in, never automatic", "None of them speaks in colour alone") before the reader saw a single thing happen. Each page keeps a one-or-two-sentence intro and then shows the grid. The displaced prose is rewritten to point at what the reader just saw rather than to set up something they have not seen yet — the colour-alone claim now names the three columns you can check it against, the delta's flat case names the MSFT row that carries it, and selection's two-slice split is explained through the two captions the reader has already watched move independently. No examples added or removed, no headings renamed (`cell-renderers#render`, `cell-renderers#telling-it-apart-in-the-dom` and `selection#selection-model` all have inbound links), and no factual claim dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(docs): give the code header something to say, and stop saying it twice The fence header was blank on every page in the docs. The design doc's call was that a lone language tag is "nothing worth its space" — but that assumed some fences would be titled, and none were: all 139 carried a language and zero carried a title, so the bar rendered as a 38px band with a Copy button floated at its right and nothing else. Show the title where an author gave one, fall back to the language, and draw the fallback as a quiet uppercase label rather than in the filename's own type. A language that names nothing (text, plaintext) still shows no tag; the bar stays, because Copy has to live somewhere other than floating over the code. A multi-file example stacked three bars between its title and its first line of code, and the third one reprinted the filename the file tab one row above was already showing. The surface now prints a filename only when nothing else names the file — ExampleShell withholds the path when it renders the tab strip, so the surface never counts files itself — and the line count and expand control move up beside the view tabs. With nothing left in it, the surface renders no header at all rather than an empty band. Measured on a production build, /docs/grid/cell-presentations: multi-file example 239px -> 201px of chrome above the first line of code, three bars down to two; fence 51px -> 51px, its one bar now reading "TSX" instead of nothing. No affordance dropped: Copy, Copy file, Copy for agent, .md, Expand and both tablists are all still present, with the roving tabindex on each tablist untouched. Records the reversal in the design doc (§1a, §1b) and replaces the tests that pinned the old decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(docs): make the code header earn its space, and lead six pages with the example - CodeSurface: show filename, else the language tag; nothing for language-less fences. Every one of the 139 fences was untitled, so the bar rendered blank rather than restrained. - ExampleShell: drop the filename when the file-tab strip already names it, and move line count + Expand into the toolbar. Three bars became two. - Title the 12 fences that map to a real file; leave fragments untitled. - Restructure six pages to lead with their example. - Fix a range-selection comment claiming a section had 'just taught' a shape it describes 13 lines later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 8827604 commit d89a681

24 files changed

Lines changed: 539 additions & 217 deletions

‎apps/website/app/components/docs/MdxRenderer.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,17 @@ interface PreProps {
2525
function Pre({ children, filename }: PreProps) {
2626
const codeProps = children.props;
2727
const raw = typeof codeProps.children === "string" ? codeProps.children : "";
28+
// Threaded explicitly rather than sniffed off the DOM downstream: the
29+
// language is a prop of the compiled `<code>` node right here, and
30+
// `CodeSurface` should not have to reach into its own children to find the
31+
// identity it renders. It is the fallback when the fence carries no
32+
// `title=` — see `CodeBlock`.
2833
return (
29-
<CodeBlock raw={raw} filename={filename}>
34+
<CodeBlock
35+
raw={raw}
36+
filename={filename}
37+
language={codeProps["data-language"]}
38+
>
3039
{children}
3140
</CodeBlock>
3241
);

‎apps/website/app/components/docs/__tests__/MdxRenderer.test.tsx‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,29 @@ describe("docsMdxComponents (fence rendering)", () => {
4545
expect(document.querySelectorAll("figure")).toHaveLength(1);
4646
});
4747

48-
it("renders a header with no identity text — but still a Copy button — for an untitled fence", async () => {
49-
const { container } = await renderFence(
50-
["```css", ":root { --y: 2; }", "```"].join("\n"),
51-
);
48+
it("falls back to the fence's own language when there is no title=", async () => {
49+
// End-to-end through the real compile pipeline, which is the only thing
50+
// that proves `data-language` is actually present on the compiled
51+
// `<code>` for `Pre` to thread through — a hand-built element tree would
52+
// just assert our own assumption back at us.
53+
await renderFence(["```css", ":root { --y: 2; }", "```"].join("\n"));
5254
expect(screen.getByRole("button", { name: /copy/i })).toBeInTheDocument();
5355
expect(document.querySelectorAll("figure")).toHaveLength(1);
54-
// No bare language tag anywhere — the exact "shows nothing worth its
55-
// space" case the design doc forbids.
56-
expect(container.textContent).not.toMatch(/^(CSS|TS|TSX|JS|JSX|BASH)$/i);
56+
const header = screen
57+
.getByRole("button", { name: /copy/i })
58+
.closest("div")!;
59+
expect(header).toHaveTextContent(/^css/i);
60+
});
61+
62+
it("prefers a fence's title= over its language", async () => {
63+
await renderFence(
64+
['```css title="brand.css"', ":root { --x: 1; }", "```"].join("\n"),
65+
);
66+
const header = screen
67+
.getByRole("button", { name: /copy/i })
68+
.closest("div")!;
69+
expect(header).toHaveTextContent(/^brand\.css/);
70+
expect(header.textContent).not.toMatch(/brand\.css\s*css/i);
5771
});
5872

5973
it("highlights the fence's actual code", async () => {

‎apps/website/app/components/docs/mdx/CodeBlock.tsx‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,33 @@ import { CodeSurface } from "./CodeSurface";
55
export interface CodeBlockProps {
66
children: ReactNode;
77
raw: string;
8+
/** From the fence's `title="…"` meta, threaded by `MdxRenderer`'s `Figure`. */
89
filename?: string;
10+
/** From rehype-pretty-code's `data-language`, threaded by `MdxRenderer`'s `Pre`. */
11+
language?: string;
912
}
1013

11-
// No `lang` prop: with the header now showing a real filename when one is
12-
// authored (via fence `title="…"` meta — see MdxRenderer's `Figure`), a bare
13-
// language tag left the bar saying nothing a reader couldn't already see
14-
// from the syntax highlighting. See the design doc's "one shared code
15-
// surface" decision.
16-
export function CodeBlock({ children, raw, filename }: CodeBlockProps) {
14+
// Titles where practicable, falling back to the language tag. The original
15+
// call was to show nothing rather than a lone language tag — but that assumed
16+
// some fences would be titled, and none were: all 139 carried a language and
17+
// zero carried a title, so every fence header rendered as a blank band with a
18+
// Copy button floated at its right. A quiet language label is worth more than
19+
// an empty bar. See the design doc's "identity in the header" decision.
20+
export function CodeBlock({
21+
children,
22+
raw,
23+
filename,
24+
language,
25+
}: CodeBlockProps) {
1726
return (
1827
<figure className="my-6 overflow-hidden rounded-md border border-rule bg-bg-card">
19-
<CodeSurface filename={filename} raw={raw} variant="fence" showCopy>
28+
<CodeSurface
29+
filename={filename}
30+
language={language}
31+
raw={raw}
32+
variant="fence"
33+
showCopy
34+
>
2035
{children}
2136
</CodeSurface>
2237
</figure>

‎apps/website/app/components/docs/mdx/CodeSurface.tsx‎

Lines changed: 74 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,28 @@ import {
88
type ReactNode,
99
} from "react";
1010

11+
/**
12+
* Languages whose tag would say nothing a reader can't already see. A `text`
13+
* fence names no language, so uppercasing it into "TEXT" would recreate the
14+
* empty-bar problem with extra ink. Everything else — ts, tsx, css, html,
15+
* bash, diff — names something real.
16+
*/
17+
const UNINFORMATIVE_LANGUAGES = new Set(["text", "plaintext", "plain", "txt"]);
18+
1119
export interface CodeSurfaceProps {
1220
/**
13-
* Shown on the left of the header. Omitted (a fence with no `title=` meta)
14-
* leaves that side blank — the bar still renders, because it still hosts
15-
* the actions on the right; it just never renders a lone language tag.
21+
* Identity shown on the left of the header. A fence gets it from `title="…"`
22+
* fence meta; an example passes its file path only when there is no file-tab
23+
* strip to name it (see `ExampleShell`), since the tabs already do.
1624
*/
1725
filename?: string;
18-
/** Full source text. Used for the Copy action and the "N lines" label. */
26+
/**
27+
* Fallback identity for a fence carrying no `title=`: the fence's own
28+
* language, drawn as a quiet uppercase label rather than a heading. Ignored
29+
* whenever `filename` is set — a real name always beats a language.
30+
*/
31+
language?: string;
32+
/** Full source text. Used for the Copy action. */
1933
raw: string;
2034
/**
2135
* A fence renders its own padding and type-scale class on the code
@@ -34,38 +48,48 @@ export interface CodeSurfaceProps {
3448
showCopy?: boolean;
3549
/**
3650
* Fixed collapsed height, in px, for the code region below the header.
37-
* Omitted renders the surface unconstrained — no fade, no line count, no
38-
* expand control. That's the fence path: fences have never had a height
39-
* problem, only examples do (48-252 line sources against a ~480px pane).
51+
* Omitted renders the surface unconstrained — no clamp, no fade. That's the
52+
* fence path: fences have never had a height problem, only examples do
53+
* (48-252 line sources against a ~480px pane).
4054
*/
4155
height?: number;
42-
/** Controlled: whether the surface is currently expanded past `height`. */
56+
/**
57+
* Controlled: whether the surface is currently expanded past `height`. The
58+
* control that flips it lives in the caller's own toolbar, not in this
59+
* header — see `ExampleShell`.
60+
*/
4361
expanded?: boolean;
44-
onToggleExpand?: () => void;
4562
/**
4663
* Fires whenever overflow status or the content's natural (unclamped)
4764
* height changes. The natural height is `header height + content
4865
* scrollHeight` — what the caller needs to grow an ancestor pane to when
49-
* the reader expands.
66+
* the reader expands. `overflowing` is what tells the caller whether to
67+
* offer an expand control at all.
5068
*/
5169
onOverflowChange?: (overflowing: boolean, naturalHeight: number) => void;
5270
children: ReactNode;
5371
}
5472

5573
/**
5674
* The one code surface shared by a fenced block (`CodeBlock`) and an
57-
* `<Example>`'s Code pane (`ExampleShell`): a header bar with file identity
58-
* on the left and actions on the right, then the code. See
75+
* `<Example>`'s Code pane (`ExampleShell`): identity on the left of a header
76+
* bar, Copy on the right, then the code. See
5977
* docs/superpowers/specs/2026-08-14-docs-code-surface-design.md.
78+
*
79+
* The header is conditional, not decorative furniture — it appears only when
80+
* it has something to say (an identity, or a Copy button). Everything an
81+
* example wraps around this surface (its own tabs, its own toolbar, its own
82+
* expand control) stays in `ExampleShell`, so this component never has to
83+
* know how many files it is one of.
6084
*/
6185
export function CodeSurface({
6286
filename,
87+
language,
6388
raw,
6489
variant,
6590
showCopy = false,
6691
height,
6792
expanded = false,
68-
onToggleExpand,
6993
onOverflowChange,
7094
children,
7195
}: CodeSurfaceProps) {
@@ -112,53 +136,50 @@ export function CodeSurface({
112136
return () => ro.disconnect();
113137
}, [height, raw, onOverflowChange]);
114138

115-
const lineCount = raw.split("\n").length;
116-
// Once expanded, the scroll region's own box grows to fit its content —
117-
// `overflowing` alone would flip back to false at that point (nothing left
118-
// to scroll to), which would yank the "Show less" control out from under
119-
// an expanded reader. `expanded` keeps both the label and the line count
120-
// in place for as long as the reader is looking at the expanded pane.
121-
const truncatable = height != null && (overflowing || expanded);
122-
const showLineCount = truncatable;
123139
const showFade = height != null && overflowing && !expanded;
124-
const showExpand = truncatable;
125140

126-
const actionButtonClass =
127-
"whitespace-nowrap rounded-[3px] border border-rule bg-bg-card px-2 py-1 font-mono text-[10px] text-text-secondary hover:text-text-primary";
141+
const languageTag =
142+
language && !UNINFORMATIVE_LANGUAGES.has(language.toLowerCase())
143+
? language
144+
: undefined;
145+
// A name always wins; the language is only the fallback. Drawing them
146+
// differently is the point: a filename is the thing's identity and reads as
147+
// written, while the language is a classification, so it gets the small
148+
// uppercase treatment this codebase already uses for quiet labels (see the
149+
// view tabs in ExampleShell) — a label on the bar, not a heading over the
150+
// code.
151+
const identity = filename ? (
152+
<span className="truncate">{filename}</span>
153+
) : languageTag ? (
154+
<span className="truncate text-[10px] uppercase tracking-[0.12em]">
155+
{languageTag}
156+
</span>
157+
) : null;
158+
159+
// No identity and no actions means an empty bar, and an empty bar is pure
160+
// cost: it was ~31px of border and background saying nothing above every
161+
// multi-file example's code. Render nothing instead. A fence always has at
162+
// least Copy, so a fence always keeps its bar.
163+
const hasHeader = identity != null || showCopy;
128164

129-
const header = (
165+
const header = hasHeader ? (
130166
<div
131167
ref={headerRef}
132-
className="flex shrink-0 items-center justify-between gap-2 border-b border-rule px-3 py-1.5 font-mono text-[11px] text-text-dim"
168+
className="flex shrink-0 items-center gap-2 border-b border-rule px-3 py-1.5 font-mono text-[11px] text-text-dim"
133169
>
134-
<span className="truncate">
135-
{filename}
136-
{filename && showLineCount ? " · " : ""}
137-
{showLineCount ? `${lineCount} lines` : ""}
138-
</span>
139-
<span className="ml-2 flex shrink-0 items-center gap-1.5">
140-
{showCopy && (
141-
<button
142-
type="button"
143-
aria-label="Copy code"
144-
onClick={onCopy}
145-
className={actionButtonClass}
146-
>
147-
{copied ? "Copied" : "Copy"}
148-
</button>
149-
)}
150-
{showExpand && (
151-
<button
152-
type="button"
153-
onClick={onToggleExpand}
154-
className={actionButtonClass}
155-
>
156-
{expanded ? "Show less" : "Expand"}
157-
</button>
158-
)}
159-
</span>
170+
{identity}
171+
{showCopy && (
172+
<button
173+
type="button"
174+
aria-label="Copy code"
175+
onClick={onCopy}
176+
className="ml-auto shrink-0 whitespace-nowrap rounded-[3px] border border-rule bg-bg-card px-2 py-1 font-mono text-[10px] text-text-secondary hover:text-text-primary"
177+
>
178+
{copied ? "Copied" : "Copy"}
179+
</button>
180+
)}
160181
</div>
161-
);
182+
) : null;
162183

163184
// The `<pre>` is load-bearing, not decorative. `MdxRenderer`'s `Pre` mapping
164185
// hands this surface rehype-pretty-code's `<code>` and drops the `<pre>` that

‎apps/website/app/components/docs/mdx/ExampleShell.tsx‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ export function ExampleShell({
7979
// — reported via `onOverflowChange` — and is what the pane grows to.
8080
const [expanded, setExpanded] = useState(false);
8181
const [naturalHeight, setNaturalHeight] = useState(0);
82+
const [overflowing, setOverflowing] = useState(false);
8283
// Stable identity: CodeSurface re-subscribes its ResizeObserver whenever
8384
// this callback's identity changes, so an inline arrow here would tear
8485
// down and recreate the observer on every ExampleShell render.
8586
const handleOverflowChange = useCallback(
86-
(_overflowing: boolean, natural: number) => {
87+
(isOverflowing: boolean, natural: number) => {
88+
setOverflowing(isOverflowing);
8789
setNaturalHeight(natural);
8890
},
8991
[],
@@ -108,6 +110,19 @@ export function ExampleShell({
108110
: (["code"] as const);
109111

110112
const file = files[active];
113+
// The file-tab strip names every file and marks the active one. When it
114+
// renders, the code surface below must NOT also print the active file's
115+
// path: that put two bars one row apart both reading
116+
// `CellPresentationsGrid.tsx`. A single-file example has no strip, so there
117+
// the surface header is the pane's only identity and keeps the name.
118+
const hasFileTabs = files.length > 1;
119+
const lineCount = file.source.split("\n").length;
120+
// Once expanded, the code's own box grows to fit its content, so
121+
// `overflowing` flips back to false (nothing left to scroll to) — which
122+
// alone would yank "Show less" out from under an expanded reader. The
123+
// `expanded` term keeps the control in place for as long as the reader is
124+
// looking at the expanded pane.
125+
const truncatable = overflowing || expanded;
111126

112127
useEffect(() => {
113128
return () => {
@@ -250,6 +265,26 @@ export function ExampleShell({
250265
</button>
251266
</div>
252267

268+
{/*
269+
The shape of the code, and the control that changes it, sit beside
270+
the tabs rather than in the surface's own header one row down. That
271+
header existed only to carry these two things for a multi-file
272+
example — the file name beside them was a duplicate of the tab
273+
already showing it — so hosting them here retires a whole bar.
274+
*/}
275+
{view === "code" && truncatable && (
276+
<div className="ml-3 flex shrink-0 flex-nowrap items-center gap-2 font-mono text-[11px] text-text-dim">
277+
<span className="whitespace-nowrap">{lineCount} lines</span>
278+
<button
279+
type="button"
280+
onClick={() => setExpanded((e) => !e)}
281+
className="whitespace-nowrap rounded-[3px] border border-rule px-2 py-1 text-[10px] text-text-secondary hover:text-text-primary"
282+
>
283+
{expanded ? "Show less" : "Expand"}
284+
</button>
285+
</div>
286+
)}
287+
253288
<div className="ml-auto flex shrink-0 flex-nowrap items-center gap-1.5 py-1.5">
254289
{view === "code" && (
255290
<button
@@ -276,7 +311,7 @@ export function ExampleShell({
276311
</div>
277312
</div>
278313

279-
{view === "code" && files.length > 1 && (
314+
{view === "code" && hasFileTabs && (
280315
<div
281316
role="tablist"
282317
aria-label="Example files"
@@ -376,12 +411,11 @@ export function ExampleShell({
376411
*/}
377412
<CodeSurface
378413
key={file.path}
379-
filename={file.path}
414+
filename={hasFileTabs ? undefined : file.path}
380415
raw={file.source}
381416
variant="example"
382417
height={height}
383418
expanded={expanded}
384-
onToggleExpand={() => setExpanded((e) => !e)}
385419
onOverflowChange={handleOverflowChange}
386420
>
387421
<div dangerouslySetInnerHTML={{ __html: file.html }} />

0 commit comments

Comments
 (0)