From 26d449d7a5d7d5e5ad4efe08c895f8a35d67c508 Mon Sep 17 00:00:00 2001 From: memoclaw <265580040+memoclaw@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:43:27 +0800 Subject: [PATCH] feat: add outline navigation to memo detail sidebar Extract h1-h4 headings from memo markdown and display them as an outline section at the top of the MemoDetail sidebar. Each heading links to its anchor via smooth scroll with hash URL updates. - Add slugify() and extractHeadings() utilities for MDAST parsing - Add rehypeHeadingId plugin to assign unique slug IDs to headings - Create MemoOutline component with level-based indentation - Integrate outline as first section in MemoDetailSidebar Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../definition.md | 35 +++++++++++ .../2026-03-23-memo-detail-outline/plan.md | 47 ++++++++++++++ web/src/components/MemoContent/index.tsx | 8 ++- .../MemoContent/markdown/Heading.tsx | 5 +- .../MemoDetailSidebar/MemoDetailSidebar.tsx | 9 +++ .../MemoDetailSidebar/MemoOutline.tsx | 52 ++++++++++++++++ web/src/locales/en.json | 1 + web/src/utils/markdown-manipulation.ts | 61 ++++++++++++++++++- .../utils/rehype-plugins/rehype-heading-id.ts | 37 +++++++++++ 9 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 docs/issues/2026-03-23-memo-detail-outline/definition.md create mode 100644 docs/issues/2026-03-23-memo-detail-outline/plan.md create mode 100644 web/src/components/MemoDetailSidebar/MemoOutline.tsx create mode 100644 web/src/utils/rehype-plugins/rehype-heading-id.ts diff --git a/docs/issues/2026-03-23-memo-detail-outline/definition.md b/docs/issues/2026-03-23-memo-detail-outline/definition.md new file mode 100644 index 000000000..58a7b7050 --- /dev/null +++ b/docs/issues/2026-03-23-memo-detail-outline/definition.md @@ -0,0 +1,35 @@ +## Background & Context + +The MemoDetail page (`web/src/pages/MemoDetail.tsx`) renders a memo's full content with a right sidebar (`MemoDetailSidebar`) on desktop. The sidebar currently shows share controls, timestamps, property badges (links, to-do, code), and tags. Memo content is rendered via `react-markdown` with custom heading components (h1–h6) in `MemoContent/markdown/Heading.tsx`. The page already has hash-based scroll-to-element infrastructure for comments (lines 39–43 of MemoDetail.tsx). + +## Issue Statement + +When viewing a memo with structured headings, there is no outline or table of contents in the MemoDetail sidebar, so readers cannot see the document structure at a glance or jump to specific sections. Heading elements rendered by the `Heading` component do not have `id` attributes, preventing any anchor-based navigation to them. + +## Current State + +- **`web/src/pages/MemoDetail.tsx`** (93 lines) — MemoDetail page. Hash-based `scrollIntoView` logic exists at lines 39–43 but only targets comment elements. The sidebar is rendered at lines 82–86 as ``. +- **`web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx`** (109 lines) — Desktop sidebar. Contains sections: Share (lines 58–65), Created-at (lines 67–76), Properties (lines 78–89), Tags (lines 91–102). Uses a reusable `SidebarSection` helper (lines 24–32). No outline section exists. +- **`web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx`** (36 lines) — Mobile drawer wrapper for the sidebar. +- **`web/src/components/MemoContent/markdown/Heading.tsx`** (30 lines) — Renders h1–h6 with Tailwind classes. Does not generate `id` attributes on heading elements. +- **`web/src/components/MemoContent/index.tsx`** (157 lines) — ReactMarkdown renderer. Maps h1–h6 to the `Heading` component (lines 72–101). No rehype-slug or equivalent plugin is installed. +- **`web/src/utils/markdown-manipulation.ts`** (143 lines) — Contains `fromMarkdown()` MDAST parsing with GFM extensions. Used for task extraction; no heading extraction exists. + +## Non-Goals + +- Redesigning the overall MemoDetail layout or sidebar structure beyond adding the outline section. +- Supporting outline for the mobile drawer — the outline section should appear in the desktop sidebar only for now. +- Adding active heading highlighting on scroll (scroll-spy behavior). +- Supporting outline in the memo list/timeline view (compact mode). +- Changing the existing heading visual styles in the content area. + +## Open Questions + +- Which heading levels to include in the outline? (default: h1–h4, matching user request) +- How to generate slug IDs — install `rehype-slug` or custom implementation? (default: lightweight custom slug generation in the Heading component to avoid a new dependency) +- Should the outline section be hidden when no headings exist? (default: yes, hide entirely like Properties/Tags sections) +- Should duplicate heading text produce unique slugs (e.g. `my-heading`, `my-heading-1`)? (default: yes, append numeric suffix for duplicates) + +## Scope + +**M** — Touches 3–4 files (Heading component, MemoDetailSidebar, possibly a new outline component, and a slug utility). Existing patterns (SidebarSection, hash scrolling) apply directly. No new subsystem or novel approach required. diff --git a/docs/issues/2026-03-23-memo-detail-outline/plan.md b/docs/issues/2026-03-23-memo-detail-outline/plan.md new file mode 100644 index 000000000..3c039677f --- /dev/null +++ b/docs/issues/2026-03-23-memo-detail-outline/plan.md @@ -0,0 +1,47 @@ +## Task List + +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] + +### T1: Add heading extraction utility [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 + +### T2: Add slug IDs to Heading component [S] + +**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 ``. +**Validation**: `cd web && pnpm lint` — no new errors + +### T3: Create MemoOutline sidebar component [M] + +**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**: +- Create: `web/src/components/MemoDetailSidebar/MemoOutline.tsx` +**Implementation**: +1. Props: `{ headings: HeadingItem[] }` from `markdown-manipulation.ts` +2. Render a `