diff --git a/.claude/agents/docs-reviewer.md b/.claude/agents/docs-reviewer.md
new file mode 100644
index 000000000..af0a856e4
--- /dev/null
+++ b/.claude/agents/docs-reviewer.md
@@ -0,0 +1,28 @@
+---
+name: docs-reviewer
+description: "Lean docs reviewer that dispatches reviews docs for a particular skill."
+model: opus
+color: cyan
+---
+
+You are a direct, critical, expert reviewer for React documentation.
+
+Your role is to use given skills to validate given doc pages for consistency, correctness, and adherence to established patterns.
+
+Complete this process:
+
+## Phase 1: Task Creation
+1. CRITICAL: Read the skill requested.
+2. Understand the skill's requirements.
+3. Create a task list to validate skills requirements.
+
+## Phase 2: Validate
+
+1. Read the docs files given.
+2. Review each file with the task list to verify.
+
+## Phase 3: Respond
+
+You must respond with a checklist of the issues you identified, and line number.
+
+DO NOT respond with passed validations, ONLY respond with the problems.
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 000000000..111403183
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,32 @@
+{
+ "skills": {
+ "suggest": [
+ {
+ "pattern": "src/content/learn/**/*.md",
+ "skill": "docs-writer-learn"
+ },
+ {
+ "pattern": "src/content/reference/**/*.md",
+ "skill": "docs-writer-reference"
+ }
+ ]
+ },
+ "permissions": {
+ "allow": [
+ "Skill(docs-voice)",
+ "Skill(docs-components)",
+ "Skill(docs-sandpack)",
+ "Skill(docs-rsc-sandpack)",
+ "Skill(docs-writer-learn)",
+ "Skill(docs-writer-reference)",
+ "Bash(yarn lint:*)",
+ "Bash(yarn lint-heading-ids:*)",
+ "Bash(yarn lint:fix:*)",
+ "Bash(yarn tsc:*)",
+ "Bash(yarn check-all:*)",
+ "Bash(yarn fix-headings:*)",
+ "Bash(yarn deadlinks:*)",
+ "Bash(yarn prettier:diff:*)"
+ ]
+ }
+}
diff --git a/.claude/skills/docs-components/SKILL.md b/.claude/skills/docs-components/SKILL.md
new file mode 100644
index 000000000..4b75f27a1
--- /dev/null
+++ b/.claude/skills/docs-components/SKILL.md
@@ -0,0 +1,518 @@
+---
+name: docs-components
+description: Comprehensive MDX component patterns (Note, Pitfall, DeepDive, Recipes, etc.) for all documentation types. Authoritative source for component usage, examples, and heading conventions.
+---
+
+# MDX Component Patterns
+
+## Quick Reference
+
+### Component Decision Tree
+
+| Need | Component |
+|------|-----------|
+| Helpful tip or terminology | `
Likes: {count}
+
+
+ {message}
+
+
);
}
diff --git a/src/components/PageHeading.tsx b/src/components/PageHeading.tsx
index 760542750..ba4b413a0 100644
--- a/src/components/PageHeading.tsx
+++ b/src/components/PageHeading.tsx
@@ -14,12 +14,16 @@ import Tag from 'components/Tag';
import {H1} from './MDX/Heading';
import type {RouteTag, RouteItem} from './Layout/getRouteMeta';
import * as React from 'react';
+import {useState, useEffect} from 'react';
+import {useRouter} from 'next/router';
import {IconCanary} from './Icon/IconCanary';
import {IconExperimental} from './Icon/IconExperimental';
+import {IconCopy} from './Icon/IconCopy';
+import {Button} from './Button';
interface PageHeadingProps {
title: string;
- version?: 'experimental' | 'canary';
+ version?: 'experimental' | 'canary' | 'rc';
experimental?: boolean;
status?: string;
description?: string;
@@ -27,6 +31,51 @@ interface PageHeadingProps {
breadcrumbs: RouteItem[];
}
+function CopyAsMarkdownButton() {
+ const {asPath} = useRouter();
+ const [copied, setCopied] = useState(false);
+
+ useEffect(() => {
+ if (!copied) return;
+ const timer = setTimeout(() => setCopied(false), 2000);
+ return () => clearTimeout(timer);
+ }, [copied]);
+
+ async function fetchPageBlob() {
+ const cleanPath = asPath.split(/[?#]/)[0];
+ const res = await fetch(cleanPath + '.md');
+ if (!res.ok) throw new Error('Failed to fetch');
+ const text = await res.text();
+ return new Blob([text], {type: 'text/plain'});
+ }
+
+ async function handleCopy() {
+ try {
+ await navigator.clipboard.write([
+ // Don't wait for the blob, or Safari will refuse clipboard access
+ new ClipboardItem({'text/plain': fetchPageBlob()}),
+ ]);
+ setCopied(true);
+ } catch {
+ // Silently fail
+ }
+ }
+
+ return (
+
+ );
+}
+
function PageHeading({
title,
status,
@@ -37,7 +86,12 @@ function PageHeading({
return (
+
+
+
+
+
+
+
+
+
+
e.stopPropagation()}
/>
setIsActive(true)}
/>
);
@@ -103,7 +103,7 @@ export default function Gallery() {
function Image() {
return (
);
@@ -124,7 +124,7 @@ img { margin: 0 10px 10px 0; }