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-dropzone": "^15.0.0",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^4.2.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SubsectionCard } from "@/components/compose/subsection-card";
|
||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import { useCase } from "@/lib/api/cases";
|
||||
import { useResearchAnalysis } from "@/lib/api/research";
|
||||
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">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed whitespace-pre-line prose">
|
||||
{content.trim()}
|
||||
</p>
|
||||
<Markdown content={content.trim()} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -216,11 +215,9 @@ export default function ComposePage({
|
||||
|
||||
{analysis.data.conclusions?.trim() && (
|
||||
<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>
|
||||
<p className="text-sm text-ink leading-relaxed whitespace-pre-line prose">
|
||||
{analysis.data.conclusions.trim()}
|
||||
</p>
|
||||
<Markdown content={analysis.data.conclusions.trim()} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChairEditor } from "@/components/compose/chair-editor";
|
||||
import { PrecedentsSection } from "@/components/compose/precedents-section";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import type { ResearchSubsection } from "@/lib/api/research";
|
||||
import type { CasePrecedent } from "@/lib/api/precedents";
|
||||
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">
|
||||
{f.label}
|
||||
</dt>
|
||||
<dd className="text-sm text-ink-soft leading-relaxed whitespace-pre-line">
|
||||
{f.content}
|
||||
<dd>
|
||||
<Markdown content={f.content} />
|
||||
</dd>
|
||||
</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