--- phase: 21-chat-foundation plan: "02" subsystem: ui tags: [chat, markdown, syntax-highlighting, rehype-highlight, components] dependency_graph: requires: ["21-00"] provides: [ChatMarkdownMessage, ChatCodeBlock] affects: [chat-message-list] tech_stack: added: [rehype-highlight@7.0.2] patterns: [TDD, ExtraProps-typed react-markdown components] key_files: created: - ui/src/components/ChatMarkdownMessage.tsx - ui/src/components/ChatCodeBlock.tsx - ui/src/components/ChatMarkdownMessage.test.tsx modified: - ui/package.json - ui/src/index.css decisions: - Use ExtraProps from react-markdown for ChatCodeBlock type signature to satisfy ComponentType constraint - Add hljs CSS as plain rules (not @import) scoped to .dark, .theme-tokyo-night, :root selectors metrics: duration: ~15 minutes completed_date: "2026-04-01" tasks: 2 files: 5 --- # Phase 21 Plan 02: Chat Markdown Renderer with Syntax Highlighting Summary **One-liner:** rehype-highlight markdown renderer with theme-aware hljs CSS overrides and ChatCodeBlock copy button using navigator.clipboard.writeText. ## Completed Tasks | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Install rehype-highlight and add hljs theme CSS overrides | 3e2bc1ae | ui/package.json, ui/src/index.css | | 2 (RED) | Add failing tests for ChatMarkdownMessage | 732032a6 | ui/src/components/ChatMarkdownMessage.test.tsx | | 2 (GREEN) | Create ChatCodeBlock and ChatMarkdownMessage components | 576e302a | ui/src/components/ChatCodeBlock.tsx, ui/src/components/ChatMarkdownMessage.tsx | ## What Was Built ### ChatMarkdownMessage Markdown renderer component wrapping react-markdown with: - `rehypePlugins={[rehypeHighlight]}` — applies hljs token classes to code blocks - `remarkPlugins={[remarkGfm]}` — GitHub Flavored Markdown - `components={{ pre: ChatCodeBlock }}` — custom code block with toolbar - `paperclip-markdown` class — picks up existing prose styles from index.css ### ChatCodeBlock Pre-element override for react-markdown that provides: - **Language label** extracted from `language-xxx` className on the child `` element - **Copy button** using `navigator.clipboard.writeText` (Copy/Check icon toggle with 1500ms success state) - **Plain pre fallback** when no code child present (e.g. plain preformatted text) - Typed using `HTMLAttributes & ExtraProps` to satisfy react-markdown's `ComponentType` constraint ### Highlight.js Theme CSS (index.css) Added 60 lines of CSS overrides covering 15 token types across three themes: - `.dark .hljs*` — Catppuccin Mocha palette (#cba6f7 keywords, #a6e3a1 strings, etc.) - `.theme-tokyo-night .hljs*` — Tokyo Night palette (#bb9af7 keywords, #9ece6a strings, etc.) - `:root .hljs*` — Catppuccin Latte palette (#8839ef keywords, #40a02b strings, etc.) No external CSS file imports — all overrides are scoped custom properties, avoiding FOUC and theme conflicts. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] Fixed ChatCodeBlock TypeScript type signature** - **Found during:** Task 2 — TypeScript type check after implementation - **Issue:** `[key: string]: unknown` index signature incompatible with `ClassAttributes & HTMLAttributes & ExtraProps` from react-markdown - **Fix:** Changed component props type to `HTMLAttributes & ExtraProps` (importing `ExtraProps` from `react-markdown`) - **Files modified:** `ui/src/components/ChatCodeBlock.tsx` - **Commit:** 576e302a (included in same commit) ## Verification Results - `pnpm --filter @paperclipai/ui exec -- tsc --noEmit` — PASS (0 errors) - `pnpm vitest run ui/src/components/ChatMarkdownMessage.test.tsx` — PASS (4/4 tests) - `rehype-highlight` present in ui/package.json — PASS - `.dark .hljs-keyword { color: #cba6f7; }` in index.css — PASS - `.theme-tokyo-night .hljs-keyword { color: #bb9af7; }` in index.css — PASS - `:root .hljs-keyword { color: #8839ef; }` in index.css — PASS - `navigator.clipboard.writeText` in ChatCodeBlock — PASS - `aria-label="Copy code"` / `"Copied!"` in ChatCodeBlock — PASS - Language extraction from `language-xxx` pattern — PASS ## Self-Check: PASSED All files created and commits verified. ## Known Stubs None — both components are fully wired. ChatMarkdownMessage uses rehype-highlight for real syntax highlighting; ChatCodeBlock calls navigator.clipboard.writeText for real copy functionality.