W3: presentation fixes — delimiters, line breaks, mermaid

- src/core/math-delims.ts: pure preprocessing for the \(...\)/\[...\] math
  contract — balanced delimiters rewrite to the two-dollar forms remark-math
  parses ($ is never math). Skips code fences/spans, escaped delimiters, and
  leaves unbalanced (mid-stream) delimiters untouched. Display math renders
  as a block: mid-paragraph gets blank-line padding; indented (list items)
  degrades to inline so list continuations don't half-parse.
- message.tsx: transform in the render path (children-memoized); math plugin
  now singleDollarTextMath: false — the currency hazard dies structurally.
  remark-breaks via the cjk remarkPluginsAfter slot (the bare remarkPlugins
  prop would replace the [remark-gfm, codeMeta] defaults).
- mermaid: dark theme + sans font via Streamdown's mermaid prop; dark-styled
  error fallback (components/mermaid-error.tsx) with error, source in
  <details>, and retry.
- globals.css (appended): @source for streamdown dist — its internal UI
  chrome (code blocks, mermaid containers, tables) was entirely unstyled
  because Tailwind v4 never scans node_modules; --color-sidebar → card; wide
  mermaid SVGs capped to the column; .katex-display scrolls.
- tests/render-markdown.test.tsx: 26 tests (transform unit + render). The
  math describe block in render-cards.test.tsx is updated to the new
  contract ($...$ now renders as literal text — pinned).
This commit is contained in:
Amos
2026-08-22 09:38:54 +03:00
parent 866352cb65
commit ef488224bc
6 changed files with 593 additions and 33 deletions
+44
View File
@@ -0,0 +1,44 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import type { MermaidErrorComponentProps } from "streamdown";
/**
* Fallback for mermaid diagrams that fail to parse — wired via Streamdown's
* `mermaid.errorComponent`. Replaces the default light-themed red box with a
* card matching the app: the error, the source in a details block (so a
* broken diagram doesn't flood the transcript), and a retry.
*/
export const MermaidError = ({
chart,
error,
retry,
className,
}: MermaidErrorComponentProps & { className?: string }) => (
<div
className={cn(
"my-4 rounded-xl border border-destructive/40 bg-destructive/5 p-4",
className,
)}
data-streamdown="mermaid-error"
>
<p className="text-sm font-medium text-destructive">
Diagram failed to render
</p>
<p className="mt-1 font-mono text-xs break-words text-muted-foreground">
{error}
</p>
<details className="mt-3">
<summary className="cursor-pointer text-xs text-muted-foreground">
Show diagram source
</summary>
<pre className="mt-2 overflow-x-auto rounded-lg bg-muted p-3 font-mono text-xs text-foreground">
{chart}
</pre>
</details>
<Button className="mt-3" onClick={retry} size="sm" variant="outline">
Retry
</Button>
</div>
);