docs/plans/2026-03-23-memo-detail-outline/plan.md
T1: Add heading extraction utility [S] — T2: Add slug IDs to Heading component [S] — T3: Create MemoOutline sidebar component [M] — T4: Integrate outline into MemoDetailSidebar [S]
Objective: Provide a function to extract h1–h4 headings from markdown content with slugified IDs, reusing the existing MDAST parsing pattern from markdown-manipulation.ts.
Files: web/src/utils/markdown-manipulation.ts
Implementation: Add HeadingItem interface (text, level, slug) and extractHeadings(markdown: string): HeadingItem[] function. Use existing fromMarkdown() + visit() pattern. Visit "heading" nodes with depth 1–4, extract text from children, generate slug via slugify() helper (lowercase, replace non-alphanumeric with hyphens, deduplicate). Export both.
Validation: cd web && pnpm lint — no new errors
Objective: Generate deterministic id attributes on h1–h6 elements so outline links can scroll to them via #hash.
Files: web/src/components/MemoContent/markdown/Heading.tsx
Implementation: In Heading (~line 13), extract text from children using a getTextContent(children) helper that recursively extracts string content from React children. Generate slug with the same slugify logic. Apply id={slug} to the rendered <Component>.
Validation: cd web && pnpm lint — no new errors
Objective: Create a modern, Claude/Linear-style outline component that renders h1–h4 headings as anchor links with indentation by level. Size: M (new component file, modern styling) Files:
web/src/components/MemoDetailSidebar/MemoOutline.tsx
Implementation:{ headings: HeadingItem[] } from markdown-manipulation.ts<nav> with vertical list of <a href="#slug"> linkspl-3, h3 pl-6, h4 pl-9. Text size: h1 text-[13px] font-medium, h2–h4 text-[13px] font-normal. Color: text-muted-foreground with hover:text-foreground transition. Left border accent line (2px) along the nav. Smooth scroll on click via scrollIntoView.block py-1 truncate transition-colors with level-based indentation
Boundaries: No scroll-spy / active state tracking. No mobile drawer integration.
Dependencies: T1
Expected Outcome: Component renders a clean, modern outline navigation.
Validation: cd web && pnpm lint — no new errorsObjective: Add the outline section as the first section in MemoDetailSidebar, shown only when headings exist.
Files: web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx
Implementation: Import extractHeadings and MemoOutline. In MemoDetailSidebar (~line 48), compute headings = useMemo(() => extractHeadings(memo.content), [memo.content]). Before the Share section (~line 58), add conditional: {headings.length > 0 && <SidebarSection label="Outline"><MemoOutline headings={headings} /></SidebarSection>}.
Validation: cd web && pnpm lint && pnpm build — no errors