--- name: godui-learn-article description: Add a "Learn" tab + tier-S design-engineer article to a GodUI component docs page. Use when writing a Learn deep-dive for a component (motion breakdown, scroll-triggered animated scenes with replay, annotated code, live result). Covers routing, sidebar hiding, the ScrollScene primitive, scene patterns, and every gotcha. --- # GodUI Learn Article A **Learn** tab sits at the right of a component page's breadcrumb row (`Docs | Learn` segmented control). `Docs` is the normal component page; `Learn` is a blog-style, scroll-animated deconstruction of how the component's motion is built — layered scenes that auto-play on scroll-in with a replay button, annotated code, and a live result. Same layout as the rest of docs (sidenav + right TOC). The tab system, routing, sidebar hiding, and the reusable `ScrollScene` primitive are **already built and generic**. Adding a Learn article to a *new* component is mostly: create the folder, write `learn.mdx`, and build a few component-specific scene components. This skill documents the whole system so you can extend it correctly. Reference implementation: **Magic Button** — `apps/docs/content/docs/components/buttons/magic-button/learn.mdx` and `apps/docs/src/components/learn/*`. ## Quick reference | Piece | File | Per-component? | |------|------|----------------| | Docs page (moved into folder) | `apps/docs/content/docs/components/{cat}/{name}/index.mdx` | ✅ create | | Learn article | `apps/docs/content/docs/components/{cat}/{name}/learn.mdx` | ✅ create | | Component-specific scenes | `apps/docs/src/components/learn/{scene}.tsx` | ✅ create | | Register scenes for MDX | `apps/docs/src/components/mdx.tsx` | ✅ add | | Reusable scene shell | `apps/docs/src/components/learn/scroll-scene.tsx` | ♻️ reuse | | Motion Score panel | `apps/docs/src/components/learn/motion-score-panel.tsx` + registry entry | ✅ add registry row (see §6.5) | | Result preview (live component) | `apps/docs/src/components/learn/result-preview.tsx` | ♻️ generalize or copy | | Tabs control | `apps/docs/src/app/docs/_components/component-tabs.tsx` | ♻️ generic | | Page routing + tab wiring | `apps/docs/src/app/docs/[[...slug]]/page.tsx` | ♻️ generic | | Keep sidebar link active on learn | `apps/docs/src/app/docs/_components/sidebar-active-link.tsx` | ♻️ generic | | Sidebar prune plugin | `apps/docs/src/lib/source.ts` → `hideLearnPagesPlugin` | ♻️ generic | | Sidebar nav order | `apps/docs/content/docs/meta.json` | ⚠️ do **not** add the learn page | ## How the system works (read before extending) ### Routing — folder + index.mdx pattern (CRITICAL) To get `/docs/components/{cat}/{name}` **and** `/docs/components/{cat}/{name}/learn`, the component must be a **folder with `index.mdx`**, not a flat `{name}.mdx` file: ``` components/buttons/magic-button/ index.mdx ← the Docs page (was magic-button.mdx) learn.mdx ← the Learn article ``` **Why not a flat sibling** (`magic-button.mdx` + `magic-button/learn.mdx`): fumadocs then treats the docs page as a *child* of the folder, so pruning `learn` still leaves a child and the sidebar renders an expandable "Magic button" group. With `index.mdx`, `folder.index` is set, pruning `learn` empties `children`, and the prune plugin collapses the folder back to a plain leaf. Always use the folder+index layout. The catch-all page (`[[...slug]]/page.tsx`) needs no change per component. It: - derives `base = slug.slice(0,3)` for `slug[0]==="components" && length>=3`; - `isLearnPage` = length 4 and last segment `learn`; - shows the Learn **tab only if `source.getPage([...base,"learn"])` exists**; - on the learn page, the breadcrumb's last crumb uses the **component** title (from `source.getPage(base)`), not the article's frontmatter title; - component badges (perf/dep) render on the docs page only (`length===3`). ### Sidebar hiding — `hideLearnPagesPlugin` `source.ts` runs a `transformPageTree.root` plugin that recursively drops any page node whose `url` ends in `/learn`, and collapses a folder emptied by that removal to its `index` leaf. Routing is unaffected (`source.getPage` still resolves the learn page). **Do not** list the learn page in `meta.json` — the explicit `pages` allowlist plus this plugin keep it out of the sidebar. This plugin is generic; new components need nothing here. ### Keeping the component link selected on the learn tab Because the learn route has no sidebar node, fumadocs marks the component's sidebar link `data-active="false"` there. `SidebarActiveLink` (client) flips it back to `"true"` (with a `MutationObserver` to survive re-renders) **only while `pathname` is still that component's `/learn`**. On leave it clears the forced flag (immediately if another sidebar item is already active) so client navigation never leaves two `data-active="true"` pills. `page.tsx` renders it when `isLearnPage`. Generic — no per-component work. ## Adding a Learn article to a new component ### 1. Convert the component page to a folder ```bash git mv apps/docs/content/docs/components/{cat}/{name}.mdx \ apps/docs/content/docs/components/{cat}/{name}/index.mdx ``` Route and content are unchanged. `meta.json` still references `components/{cat}/{name}` and resolves to the folder index — leave it as-is. ### 2. Read the real component source The article must be grounded in the actual implementation, not invented. Read `packages/components/src/ui/{name}.tsx` (core) or `packages/lab/src/{name}/{name}.tsx` (Lab) and note the true mechanisms: transforms, transition timings/beziers, keyframes, observers, a11y (keyboard/`focus-visible`/ `motion-reduce`). Every claim and code excerpt in the article comes from here. ### 3. Build component-specific scenes Each interactive block wraps the shared `ScrollScene`. Its render-prop hands you `{ cycle, reduced }`: - **`cycle`** — 0 before scroll-in, 1 on scroll-in, +1 per replay. **Use it as a React `key`** on the animated subtree so CSS keyframes restart on replay. - **`reduced`** — `prefers-reduced-motion`; render the resolved (final) state with no animation. **Reliable replay = keyframes + `key={cycle}` remount.** Do NOT drive scenes with a transition toggled by a boolean — that replay is unreliable. Use CSS `@keyframes` (in an inline `