Phase 3c: Compose view with chair-position editor
New /cases/[caseNumber]/compose route ports the research analysis +
chair-position editing flow from the vanilla UI onto the Next.js
stack. Reads /api/cases/{n}/research/analysis, renders background
prose in the side column and threshold claims + issues as collapsible
cards in the main column, each with a blur-autosaved chair editor
wired through a TanStack Query mutation with optimistic cache patching
(so concurrent reads don't steal editor focus).
Handles the common "analysis not yet generated" 404 with a dedicated
empty state rather than an error card.
Phase 3 task 85 is now ready for review end-to-end.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
web-ui/src/components/compose/subsection-card.tsx
Normal file
88
web-ui/src/components/compose/subsection-card.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChairEditor } from "@/components/compose/chair-editor";
|
||||
import type { ResearchSubsection } from "@/lib/api/research";
|
||||
|
||||
export function SubsectionCard({
|
||||
caseNumber,
|
||||
item,
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
caseNumber: string;
|
||||
item: ResearchSubsection;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const isFilled = Boolean(item.chair_position?.trim());
|
||||
|
||||
return (
|
||||
<article className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
className="
|
||||
w-full flex items-center gap-3 px-4 py-3 text-right
|
||||
hover:bg-gold-wash/30 transition-colors
|
||||
focus:outline-none focus-visible:bg-gold-wash/40
|
||||
"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span
|
||||
className="
|
||||
inline-flex items-center justify-center shrink-0
|
||||
w-7 h-7 rounded-full
|
||||
bg-navy text-parchment font-display font-bold text-sm
|
||||
tabular-nums
|
||||
"
|
||||
>
|
||||
{item.number}
|
||||
</span>
|
||||
<span className="flex-1 text-navy font-semibold text-base leading-snug">
|
||||
{item.title}
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-[0.72rem] rounded-full px-2.5 py-0.5 border shrink-0
|
||||
${
|
||||
isFilled
|
||||
? "bg-success-bg text-success border-success/40"
|
||||
: "bg-rule-soft text-ink-muted border-rule"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{isFilled ? "✓ עמדה נקבעה" : "ממתין לעמדה"}
|
||||
</span>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-ink-muted transition-transform ${open ? "rotate-180" : ""}`}
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="border-t border-rule px-5 py-4 space-y-4 bg-parchment/40">
|
||||
{item.fields.length > 0 && (
|
||||
<dl className="space-y-3">
|
||||
{item.fields.map((f, i) => (
|
||||
<div key={i}>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)}
|
||||
<ChairEditor
|
||||
caseNumber={caseNumber}
|
||||
sectionId={item.id}
|
||||
initialValue={item.chair_position ?? ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user