Files
legal-ai/web-ui/src/components/cases/case-header.tsx
Chaim 80809ca406
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
fix(ui): case header — keep metadata parallel to title, clamp parties to 2 lines
The hearing-date/updated/sync metadata dl dropped below the title (taking rows and
pushing the band down) when the appellants/respondents line was long, because the
outer row used flex-wrap. Fix:

- Drop flex-wrap on the title↔metadata row (and flex-1 on the title block) so the
  metadata dl stays parallel to the H1; the title block shrinks instead.
- Clamp the parties line (and the subject fallback) to 2 lines with line-clamp-2 +
  title tooltip, so a long party list no longer grows the band.

Chair-directed layout fix; existing components, no new design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:11:32 +00:00

160 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from "react";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/cases/status-badge";
import { SyncIndicator } from "@/components/cases/sync-indicator";
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
import { CreateRepoButton } from "@/components/cases/create-repo-button";
import {
PRACTICE_AREA_LABELS,
APPEAL_SUBTYPE_LABELS,
isBlamSubtype,
} from "@/lib/practice-area";
import type { CaseDetail } from "@/lib/api/cases";
function formatDate(iso?: string | null) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso ?? "—";
}
}
function partiesLine(data?: CaseDetail): string | null {
const appellant = data?.appellants?.filter(Boolean) ?? [];
const respondent = data?.respondents?.filter(Boolean) ?? [];
const parts: string[] = [];
if (appellant.length) parts.push(`עוררת: ${appellant.join(", ")}`);
if (respondent.length) parts.push(`משיבה: ${respondent.join(", ")}`);
return parts.length ? parts.join(" · ") : null;
}
/**
* Case header — parchment band (IA-redesign mockup 17): full-bleed band with
* the case title + status/type chips inline, a parties line, the case actions
* (edit / archive / repo / sync), and a metadata strip. The `tabs` slot renders
* the tab strip inside the band, anchored to its bottom edge.
*/
export function CaseHeader({
data,
actions,
tabs,
}: {
data?: CaseDetail;
actions?: ReactNode;
tabs?: ReactNode;
}) {
const parties = partiesLine(data);
const isBlam =
data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype);
return (
<div className="-mx-10 -mt-10 mb-2 bg-parchment border-b border-rule px-10 pt-6">
<nav className="text-[0.78rem] text-ink-muted mb-3 flex items-center gap-2">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden>·</span>
<span>תיקי ערר</span>
<span aria-hidden>·</span>
<span className="text-navy tabular-nums">{data?.case_number ?? "…"}</span>
</nav>
{/* title block + metadata on one row — no wrap, so the metadata dl stays
parallel to the H1 and a long parties line can't push it below. */}
<div className="flex items-start justify-between gap-6">
<div className="min-w-0 flex-1">
{/* title row — H1 + status/type/blam chips inline (mockup .band h1) */}
<h1 className="text-navy text-[1.7rem] font-bold leading-tight flex items-center gap-3 flex-wrap mb-0">
<span className="tabular-nums">
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
</span>
{data?.status && <StatusBadge status={data.status} />}
{data?.archived_at && (
<Badge
variant="outline"
className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-ink-muted/10 text-ink-muted border-ink-muted/30"
>
בארכיון
</Badge>
)}
{data?.practice_area && (
<Badge
variant="outline"
className="rounded-full px-3 py-0.5 text-[0.75rem] font-semibold bg-gold-wash text-gold-deep border-rule"
>
{PRACTICE_AREA_LABELS[data.practice_area]}
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
)}
</Badge>
)}
{isBlam && (
<Badge
variant="outline"
className="rounded-full px-3 py-0.5 text-[0.75rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
title="בקשה להארכת מועד להגשת ערר"
>
בל&quot;מ
</Badge>
)}
</h1>
{/* case title / subject under the heading */}
{data?.title && (
<p className="text-navy/90 text-base font-semibold mt-2 max-w-3xl leading-snug">
{data.title}
</p>
)}
{/* parties line (mockup .parties) — clamped to 2 lines so a long list
of appellants/respondents doesn't grow the band; full text on hover. */}
{parties ? (
<p className="text-ink-soft text-sm mt-1.5 max-w-3xl line-clamp-2" title={parties}>
{parties}
</p>
) : data?.subject ? (
<p className="text-ink-soft text-sm mt-1.5 max-w-3xl leading-relaxed line-clamp-2"
title={data.subject}>
{data.subject}
</p>
) : null}
{/* case actions — kept verbatim, moved into the band */}
<div className="flex items-center gap-2 flex-wrap mt-3">
{data?.case_number && (
<CaseArchiveAction
caseNumber={data.case_number}
archivedAt={data.archived_at}
/>
)}
<CreateRepoButton data={data} />
{actions}
</div>
</div>
{/* metadata strip — hearing date / updated / sync */}
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm shrink-0">
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
תאריך דיון
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
עודכן
</dt>
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
סנכרון
</dt>
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
</dl>
</div>
{/* tab strip anchored to band bottom (mockup .tabs) */}
{tabs ? <div className="mt-5">{tabs}</div> : null}
</div>
);
}