Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/penpal/ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ see-also:
- <a id="E-PENPAL-MD-RENDER"></a>**E-PENPAL-MD-RENDER**: Each rendered block is tagged with `data-source-line` (1-indexed). Heading IDs use the same slugification algorithm as Go's goldmark renderer. Mermaid blocks produce `.mermaid-container` divs with `data-mermaid-source`.
← [P-PENPAL-GFM](PRODUCT.md#P-PENPAL-GFM), [P-PENPAL-MERMAID](PRODUCT.md#P-PENPAL-MERMAID)

- <a id="E-PENPAL-MD-STABLE-COMPONENTS"></a>**E-PENPAL-MD-STABLE-COMPONENTS**: MarkdownViewer's custom `components` object passed to ReactMarkdown must use stable function references across re-renders. The `code` component reads `highlights` through a ref instead of a closure so that `useMemo` has no dependencies. This prevents ReactMarkdown from unmounting/remounting custom components when highlights change, which would destroy externally-rendered mermaid SVGs and their highlight overlays.
← [P-PENPAL-MERMAID](PRODUCT.md#P-PENPAL-MERMAID), [P-PENPAL-SVG-HIGHLIGHT](PRODUCT.md#P-PENPAL-SVG-HIGHLIGHT)

- <a id="E-PENPAL-HOME-SIDEBAR"></a>**E-PENPAL-HOME-SIDEBAR**: In home mode (no active project), `Layout.tsx` renders a sidebar tree with: ⌂ header with view-options button and "+" add button, expandable workspace items (with child projects), standalone project items as top-level peers, and global "In Review" / "Recent" `NavLink`s in a global nav section. Visual dividers (`.home-section-divider`) separate the workspaces section, standalone projects section, and global navigation section; sections with no items are omitted along with their dividers.
← [P-PENPAL-HOME](PRODUCT.md#P-PENPAL-HOME), [P-PENPAL-HOME-TREE](PRODUCT.md#P-PENPAL-HOME-TREE)

Expand Down
1 change: 1 addition & 0 deletions apps/penpal/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ see-also:
| Git Integration (P-PENPAL-GIT-INFO) | — | — | — | — |
| File List & Grouping (P-PENPAL-FILE-LIST) | — | ProjectPage.test.tsx | grouping_test.go, integration_test.go | — |
| Markdown Rendering (P-PENPAL-GFM, MERMAID) | — | MarkdownViewer.test.tsx | — | mermaid-comments.spec.ts |
| Stable Components (E-PENPAL-MD-STABLE-COMPONENTS) | — | MarkdownViewer.test.tsx | — | — |
| Text Selection & Anchors (P-PENPAL-SELECT-COMMENT, ANCHOR) | — | SelectionToolbar.test.tsx | — | review-workflow.spec.ts |
| Anchor Resolution (P-PENPAL-ANCHOR-RESOLVE) | comments_test.go (implicit via round-trip) | rehypeCommentHighlights.test.ts | — | — |
| Comment Highlights (P-PENPAL-HIGHLIGHT) | — | rehypeCommentHighlights.test.ts, MarkdownViewer.test.tsx | — | review-workflow.spec.ts |
Expand Down
22 changes: 22 additions & 0 deletions apps/penpal/frontend/src/components/MarkdownViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,28 @@ describe('MarkdownViewer', () => {
expect(codeMarks.length).toBeGreaterThan(0);
});

// E-PENPAL-MD-STABLE-COMPONENTS: mermaid containers survive highlight changes
it('preserves mermaid containers when highlights change', () => {
const md = '```mermaid\ngraph TD\n A --> B\n```\n\nSome text here';
const { container, rerender } = render(
<MarkdownViewer content={md} rawMarkdown={md} />,
);
const mermaidBefore = container.querySelector('.mermaid-container');
expect(mermaidBefore).not.toBeNull();

// Re-render with highlights — mermaid container must be the same DOM node
const highlights = [
{ threadId: 't1', selectedText: 'Some text', startLine: 6 },
];
rerender(
<MarkdownViewer content={md} rawMarkdown={md} highlights={highlights} />,
);
const mermaidAfter = container.querySelector('.mermaid-container');
expect(mermaidAfter).not.toBeNull();
// Same DOM element reference means React reconciled in place (not unmounted/remounted)
expect(mermaidAfter).toBe(mermaidBefore);
});

it('renders pending highlights with pending-highlight class', () => {
const md = 'Hello world';
const highlights = [
Expand Down
11 changes: 9 additions & 2 deletions apps/penpal/frontend/src/components/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(
function MarkdownViewer({ content, rawMarkdown: _rawMarkdown, onHeadingsExtracted, className, highlights }, ref) {
const innerRef = useRef<HTMLDivElement>(null);

// E-PENPAL-MD-STABLE-COMPONENTS: keep highlights in a ref so the code
// component reads the latest value without being recreated on every change.
// This prevents ReactMarkdown from unmounting/remounting custom components
// (which would destroy externally-rendered mermaid SVGs).
const highlightsRef = useRef<ThreadHighlight[]>([]);
highlightsRef.current = highlights ?? [];

// Expose the inner ref to the parent
useImperativeHandle(ref, () => innerRef.current!, []);

Expand Down Expand Up @@ -244,7 +251,7 @@ const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(

// Find highlights targeting lines within this code block
const codeHighlights: { selectedText: string; threadId: string; pending?: boolean; occurrenceIndex?: number }[] =
(highlights ?? []).filter(hl => {
highlightsRef.current.filter(hl => {
if (!sourceLine) return false;
// Code block spans from sourceLine (``` fence) to sourceLine + codeLineCount + 1 (closing ```)
// Include fence line (>=) so anchors resolved to the opening ``` aren't silently dropped
Expand Down Expand Up @@ -301,7 +308,7 @@ const MarkdownViewer = forwardRef<HTMLDivElement, MarkdownViewerProps>(
return <pre {...domProps}>{children}</pre>;
},
}),
[highlights],
[], // E-PENPAL-MD-STABLE-COMPONENTS: stable references prevent mermaid SVG destruction
);

return (
Expand Down