Render research prose as Markdown with RTL tables
The analysis-and-research.md content has markdown tables (ציר דיוני) and inline formatting like **label:** strong runs, which were rendering as raw pipes and dashes because the compose page used whitespace-pre-line on plain text. Add a reusable <Markdown> component backed by react-markdown + remark-gfm with a custom `components` map that styles paragraphs, lists, blockquotes, strong runs, and especially GFM tables for RTL + aligned columns: - table: table-auto + border-collapse, wrapped in overflow-x-auto so very wide tables don't push the parent card out - th: whitespace-nowrap so the header row sets column widths and every row border lines up row-to-row - everything text-right + the whole block gets dir="rtl" at the root Use it in three places on the compose screen: - ProseSection (represented_party, procedural_background, agreed_facts, disputed_facts) - Conclusions card - SubsectionCard field content — threshold_claims and issues have the same markdown shape in their fields[] react-markdown + remark-gfm added (~30KB gzipped). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
web-ui/src/components/ui/markdown.tsx
Normal file
116
web-ui/src/components/ui/markdown.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
/*
|
||||
* Tiny markdown renderer for Hebrew prose blocks — paragraphs, lists,
|
||||
* emphasis, and GFM tables (the main reason this exists). The parsed
|
||||
* research_md fields and the conclusions field both contain tables
|
||||
* like "ציר דיוני" that we want to render as real <table>s, RTL, with
|
||||
* auto-sized columns that line up row-to-row.
|
||||
*
|
||||
* Table styling uses `table-auto` + `whitespace-nowrap` on header cells
|
||||
* so the column widths are dictated by the longest cell in that column,
|
||||
* and every row's borders align exactly underneath each other. The
|
||||
* overflow-x-auto wrapper catches extremely wide tables on narrow
|
||||
* viewports without letting the parent card grow.
|
||||
*/
|
||||
|
||||
export function Markdown({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="prose-md text-sm text-ink-soft leading-relaxed" dir="rtl">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ node: _n, ...props }) => (
|
||||
<p className="mb-2 last:mb-0 text-justify" {...props} />
|
||||
),
|
||||
strong: ({ node: _n, ...props }) => (
|
||||
<strong className="text-navy font-semibold" {...props} />
|
||||
),
|
||||
em: ({ node: _n, ...props }) => (
|
||||
<em className="text-ink" {...props} />
|
||||
),
|
||||
a: ({ node: _n, ...props }) => (
|
||||
<a
|
||||
className="text-gold-deep hover:text-gold underline underline-offset-2"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
ul: ({ node: _n, ...props }) => (
|
||||
<ul className="list-disc ps-5 mb-2 space-y-1" {...props} />
|
||||
),
|
||||
ol: ({ node: _n, ...props }) => (
|
||||
<ol className="list-decimal ps-5 mb-2 space-y-1" {...props} />
|
||||
),
|
||||
li: ({ node: _n, ...props }) => (
|
||||
<li className="text-ink" {...props} />
|
||||
),
|
||||
h1: ({ node: _n, ...props }) => (
|
||||
<h3 className="text-navy text-base font-semibold mt-3 mb-1" {...props} />
|
||||
),
|
||||
h2: ({ node: _n, ...props }) => (
|
||||
<h4 className="text-navy text-sm font-semibold mt-3 mb-1" {...props} />
|
||||
),
|
||||
h3: ({ node: _n, ...props }) => (
|
||||
<h5 className="text-navy text-sm font-semibold mt-2 mb-1" {...props} />
|
||||
),
|
||||
blockquote: ({ node: _n, ...props }) => (
|
||||
<blockquote
|
||||
className="border-e-2 border-gold-soft pe-3 text-ink italic my-2"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
code: ({ node: _n, ...props }) => (
|
||||
<code
|
||||
className="rounded bg-rule-soft px-1 py-0.5 font-mono text-[0.78rem] text-ink"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
/* ── Tables ─────────────────────────────────────────────────
|
||||
Wrapped in an overflow-x-auto so very wide tables don't push
|
||||
the parent card out of its track. table-auto lets the browser
|
||||
size columns by their longest cell (that's what keeps borders
|
||||
aligned row-to-row) and whitespace-nowrap on the headers
|
||||
ensures the header row sets column widths instead of
|
||||
breaking mid-word. */
|
||||
table: ({ node: _n, ...props }) => (
|
||||
<div className="my-3 -mx-1 overflow-x-auto">
|
||||
<table
|
||||
className="w-full table-auto border-collapse border border-rule text-sm text-right"
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
thead: ({ node: _n, ...props }) => (
|
||||
<thead className="bg-rule-soft/70" {...props} />
|
||||
),
|
||||
tbody: ({ node: _n, ...props }) => <tbody {...props} />,
|
||||
tr: ({ node: _n, ...props }) => (
|
||||
<tr className="border-b border-rule last:border-b-0" {...props} />
|
||||
),
|
||||
th: ({ node: _n, ...props }) => (
|
||||
<th
|
||||
className="border border-rule px-3 py-2 text-right text-navy font-semibold whitespace-nowrap align-top"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ node: _n, ...props }) => (
|
||||
<td
|
||||
className="border border-rule px-3 py-2 text-right text-ink align-top"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
hr: ({ node: _n, ...props }) => (
|
||||
<hr className="my-3 border-rule" {...props} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user