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:
1460
web-ui/package-lock.json
generated
1460
web-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,8 @@
|
|||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-dropzone": "^15.0.0",
|
"react-dropzone": "^15.0.0",
|
||||||
"react-hook-form": "^7.72.1",
|
"react-hook-form": "^7.72.1",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"shadcn": "^4.2.0",
|
"shadcn": "^4.2.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { SubsectionCard } from "@/components/compose/subsection-card";
|
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||||
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import { useCase } from "@/lib/api/cases";
|
import { useCase } from "@/lib/api/cases";
|
||||||
import { useResearchAnalysis } from "@/lib/api/research";
|
import { useResearchAnalysis } from "@/lib/api/research";
|
||||||
import { useCasePrecedents } from "@/lib/api/precedents";
|
import { useCasePrecedents } from "@/lib/api/precedents";
|
||||||
@@ -19,9 +20,7 @@ function ProseSection({ title, content }: { title: string; content?: string }) {
|
|||||||
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
|
<h3 className="text-[0.78rem] uppercase tracking-[0.08em] text-gold-deep font-semibold">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-ink-soft leading-relaxed whitespace-pre-line prose">
|
<Markdown content={content.trim()} />
|
||||||
{content.trim()}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,11 +215,9 @@ export default function ComposePage({
|
|||||||
|
|
||||||
{analysis.data.conclusions?.trim() && (
|
{analysis.data.conclusions?.trim() && (
|
||||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||||
<CardContent className="px-6 py-5 space-y-2">
|
<CardContent className="px-6 py-5 space-y-3">
|
||||||
<h2 className="text-gold-deep text-xl mb-0">מסקנות</h2>
|
<h2 className="text-gold-deep text-xl mb-0">מסקנות</h2>
|
||||||
<p className="text-sm text-ink leading-relaxed whitespace-pre-line prose">
|
<Markdown content={analysis.data.conclusions.trim()} />
|
||||||
{analysis.data.conclusions.trim()}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { ChairEditor } from "@/components/compose/chair-editor";
|
import { ChairEditor } from "@/components/compose/chair-editor";
|
||||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||||
|
import { Markdown } from "@/components/ui/markdown";
|
||||||
import type { ResearchSubsection } from "@/lib/api/research";
|
import type { ResearchSubsection } from "@/lib/api/research";
|
||||||
import type { CasePrecedent } from "@/lib/api/precedents";
|
import type { CasePrecedent } from "@/lib/api/precedents";
|
||||||
import type { PracticeArea } from "@/lib/practice-area";
|
import type { PracticeArea } from "@/lib/practice-area";
|
||||||
@@ -76,8 +77,8 @@ export function SubsectionCard({
|
|||||||
<dt className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-semibold mb-1">
|
<dt className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-semibold mb-1">
|
||||||
{f.label}
|
{f.label}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="text-sm text-ink-soft leading-relaxed whitespace-pre-line">
|
<dd>
|
||||||
{f.content}
|
<Markdown content={f.content} />
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
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