Skip to content

Commit 8789210

Browse files
committed
fix(shell-docs): rendering parity, copy dedent, and content cleanup
Consolidates four QA findings from the post-cutover rendering pipeline: - MdxCodeBlock now dedents fence-in-JSX bodies before passing the copy payload to <CopyButton>. Fences nested inside <Tab>/<Step>/<Tabs> were reaching the clipboard with 16-24 leading spaces per line (invalid Python / TS on paste). The visible <pre> body is dedented in lockstep so rendering matches what's copied. - Wires rehypeCodeMeta + pre: MdxCodeBlock into the framework-root after-features MDX renderer, achieving parity with docs-page-view.tsx. Previously bare rehypeHighlight left these blocks without copy buttons or file-path captions. - Closes the two fences in display-only.mdx with three backticks instead of four, so the fences actually terminate and copy doesn't grab adjacent JSX/prose. - Removes a copy-paste-duplicated <Callout> on tool-rendering.mdx (kept the first occurrence, dropped the post-InlineDemo duplicate).
1 parent 7bed352 commit 8789210

4 files changed

Lines changed: 70 additions & 10 deletions

File tree

showcase/shell-docs/src/app/[framework]/[[...slug]]/page.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ import { MDXRemote } from "next-mdx-remote/rsc";
2222
import remarkGfm from "remark-gfm";
2323
import rehypeHighlight from "rehype-highlight";
2424
import { DocsPageView } from "@/components/docs-page-view";
25+
import { MdxCodeBlock } from "@/components/mdx-code-block";
2526
import { SidebarFrameworkSelector } from "@/components/sidebar-framework-selector";
2627
import { SidebarNav } from "@/components/sidebar-nav";
2728
import { UnscopedDocsPage } from "@/components/unscoped-docs-page";
2829
import { FrameworkOverview } from "@/components/content/landing-pages/framework-overview";
2930
import { frameworkOverviews } from "@/data/frameworks";
3031
import { docsComponents } from "@/lib/mdx-registry";
32+
import { rehypeCodeMeta } from "@/lib/rehype-code-meta";
3133
import {
3234
CONTENT_DIR,
3335
buildFrameworkOverridesNav,
@@ -444,11 +446,26 @@ async function FrameworkRootPage({ framework }: { framework: string }) {
444446
afterFeatures = (
445447
<MDXRemote
446448
source={raw}
447-
components={docsComponents}
449+
components={{
450+
...docsComponents,
451+
// Mirror DocsPageView: wrap MDX-rendered <pre> blocks
452+
// with figure chrome (copy button + optional file-path
453+
// caption) so fenced code in after-features.mdx has the
454+
// same affordances as fenced code on a regular docs
455+
// page. `rehypeCodeMeta` (below) supplies the
456+
// `data-title` / `data-language` data-attrs MdxCodeBlock
457+
// reads.
458+
pre: MdxCodeBlock,
459+
}}
448460
options={{
449461
mdxOptions: {
450462
remarkPlugins: [remarkGfm],
451-
rehypePlugins: [rehypeHighlight],
463+
// `rehypeCodeMeta` runs AFTER rehype-highlight so it
464+
// sees the `language-<name>` className and the fence
465+
// metastring's `title="..."`; without it, the bare
466+
// rehype-highlight wiring leaves <pre> with no copy
467+
// button and no caption (PR #4830 parity).
468+
rehypePlugins: [rehypeHighlight, rehypeCodeMeta],
452469
},
453470
}}
454471
/>

showcase/shell-docs/src/components/mdx-code-block.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,34 @@ function extractText(node: React.ReactNode): string {
5151
return "";
5252
}
5353

54+
/**
55+
* Strip uniform leading whitespace from a multi-line code body.
56+
*
57+
* Why: when a triple-fenced block sits inside JSX (e.g. ```python inside
58+
* `<Tab>`/`<Step>`), MDX preserves the JSX nesting's leading indent on
59+
* every body line. extractText() recovers that text faithfully, so the
60+
* clipboard payload comes out with 16-24 leading spaces per line and
61+
* pasted code is invalid. This helper measures the minimum indent
62+
* across non-blank lines and strips it from every line — turning a
63+
* uniformly-indented block back into column-0 code.
64+
*
65+
* Tabs are counted as 1 unit (we don't expand to N spaces); the goal
66+
* is to fix uniform-indent leakage, not normalize mixed indentation.
67+
* Blank lines are left as-is rather than over-trimmed.
68+
*/
69+
function dedent(text: string): string {
70+
const lines = text.split("\n");
71+
const nonBlank = lines.filter((l) => l.trim().length > 0);
72+
if (nonBlank.length === 0) return text;
73+
const minIndent = Math.min(
74+
...nonBlank.map((l) => l.match(/^[\s]*/)![0].length),
75+
);
76+
if (minIndent === 0) return text;
77+
return lines
78+
.map((l) => (l.length >= minIndent ? l.slice(minIndent) : l))
79+
.join("\n");
80+
}
81+
5482
export function MdxCodeBlock(props: MdxCodeBlockProps) {
5583
const {
5684
"data-title": title,
@@ -64,7 +92,7 @@ export function MdxCodeBlock(props: MdxCodeBlockProps) {
6492
// rehype-highlight always emits a single <code> child, but we walk
6593
// defensively so an unhighlighted fallback (plain text child) still
6694
// copies correctly.
67-
const codeText = (() => {
95+
const rawCodeText = (() => {
6896
const kids = Children.toArray(children);
6997
const codeEl = kids.find(
7098
(k) => isValidElement(k) && (k.type === "code" || k.type === "CODE"),
@@ -77,6 +105,15 @@ export function MdxCodeBlock(props: MdxCodeBlockProps) {
77105
return extractText(children);
78106
})();
79107

108+
// Strip uniform JSX-nesting indent (see `dedent` doc-comment above).
109+
// When `codeText !== rawCodeText`, a non-zero indent was leaking from
110+
// the fence's JSX container — in that case we also need to dedent the
111+
// visible <pre> body so the on-page render matches the copy payload.
112+
// Otherwise the user sees correctly-indented code in clipboard but
113+
// 24-sp-indented code on the page (or vice versa).
114+
const codeText = dedent(rawCodeText);
115+
const indentLeaked = codeText !== rawCodeText;
116+
80117
// No title and no need for chrome? Fall through to the global
81118
// `.reference-content pre` styling — keeps backwards compatibility with
82119
// pages that don't expect a header strip while still surfacing the copy
@@ -106,7 +143,17 @@ export function MdxCodeBlock(props: MdxCodeBlockProps) {
106143
{...rest}
107144
className={`mdx-code-block__pre text-[12.5px] leading-[1.55] overflow-x-auto p-4 m-0 ${className ?? ""}`}
108145
>
109-
{children}
146+
{indentLeaked ? (
147+
// Fence body was uniformly indented (JSX-nested fence). Render
148+
// the dedented plain text so what the user sees matches what
149+
// they copy. We lose syntax highlighting on these blocks, but
150+
// the alternative is indented-and-invalid code on screen.
151+
<code className={`language-${language ?? "plaintext"}`}>
152+
{codeText}
153+
</code>
154+
) : (
155+
children
156+
)}
110157
</pre>
111158
</div>
112159
</figure>

showcase/shell-docs/src/content/docs/generative-ui/tool-rendering.mdx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ rendering**.
2020

2121
<InlineDemo demo="tool-rendering" />
2222

23-
<Callout type="info">
24-
**Free course:** See this pattern built end-to-end in [Build Interactive Agents with Generative UI](https://www.deeplearning.ai/short-courses/build-interactive-agents-with-generative-ui/) — a free DeepLearning.AI short course taught by CopilotKit's CEO covering the full Generative UI spectrum (Controlled, Declarative, and Open-Ended).
25-
</Callout>
26-
2723
## When should I use this?
2824

2925
Render tool calls when you want to:

showcase/shell-docs/src/content/docs/generative-ui/your-components/display-only.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ parameters: ChartProps,
2727
render: Chart
2828
});
2929

30-
````
30+
```
3131
</Tab>
3232
<Tab value="chart.tsx">
3333
```tsx
@@ -50,7 +50,7 @@ export function Chart({ title, data }: z.infer<typeof ChartProps>) {
5050
</div>
5151
);
5252
}
53-
````
53+
```
5454

5555
</Tab>
5656
</Tabs>

0 commit comments

Comments
 (0)