feat(ui): WS6 — party accordion, full-width compose, status-hero overview (#206)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s

שלושת שינויי-ה-UI של WS6 לפי המוקאפים המאושרים (25b/03b/18b, X17):

1. טאב "טיעונים" (legal-arguments-panel) — אקורדיון ברמת-הצד, סגור
   כברירת-מחדל (type="multiple" ללא defaultValue). כל trigger מציג
   שם-צד + תת-כותרת + באדג'-מונה. בתוך כל צד נשמר הקיבוץ-לפי-קדימות
   והאקורדיון הפנימי ברמת-הטיעון.

2. טאב "עמדות וטענות" (compose) — רוחב-דף מלא: בוטל ה-grid 1fr/320px,
   ראיל "מסמכי התיק" הוסר (DocumentsPanel בסקירה הוא הבעלים היחיד)
   ועבר לפס-מסמכים מתקפל למעלה (סגור כברירת-מחדל). כרטיסי-הסוגיות
   2-up. פסיקה-מצורפת ושערי-הייצוא/העלאה שומרו (relocate, לא remove).
   מפת-StatusChip המקבילה הוסרה → StatusBadge המשותף (G2).

3. סטטוס רק בסקירה (overview) — StatusHero חדש (stepper אופקי + chip
   שלב-נוכחי + שינוי-סטטוס ידני + meta-strip), הבעלים היחיד של
   הסטטוס. WorkflowTimeline/StatusChanger/AgentStatusWidget ירדו משאר
   הטאבים; הטאבים האחרים רוחב-מלא. StatusBadge read-only נשאר ב-H1.

Invariants: INV-IA1 (בעלים-יחיד לסטטוס=hero), INV-IA3/G10 (כל שער-אנוש
נשמר במקום אחד — שינוי-סטטוס בסקירה, ייצוא/העלאה ב-compose),
INV-IA4 (compose=מרחב-עבודה ממוקד-משימה רוחב-מלא), INV-IA6 (מפת-סטטוסים
progressive-disclosure), INV-UI7/UI8, G2 (StatusBadge משותף, אין
מפת-סטטוס מקבילה).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 12:22:06 +00:00
parent 4feac756ba
commit f1e9103723
5 changed files with 485 additions and 300 deletions

View File

@@ -7,13 +7,20 @@ import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { SubsectionCard } from "@/components/compose/subsection-card";
import { PrecedentsSection } from "@/components/compose/precedents-section";
import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel";
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
import { StatusBadge } from "@/components/cases/status-badge";
import { Markdown } from "@/components/ui/markdown";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useCase, type CaseStatus } from "@/lib/api/cases";
import { useCase } from "@/lib/api/cases";
import {
useCaseLearningStatus,
type CaseLearningStatus,
@@ -23,34 +30,6 @@ import { useCasePrecedents } from "@/lib/api/precedents";
import { APPEAL_SUBTYPES } from "@/lib/practice-area";
import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types";
// ── Case-status → Hebrew label + tone (mockup 03 status chip) ────────────────
const STATUS_CHIP: Record<string, { label: string; cls: string }> = {
new: { label: "חדש", cls: "bg-rule-soft text-ink-muted border-rule" },
processing: { label: "בעיבוד", cls: "bg-info-bg text-info border-info/30" },
documents_ready: { label: "מסמכים מוכנים", cls: "bg-info-bg text-info border-info/30" },
outcome_set: { label: "תוצאה נקבעה", cls: "bg-info-bg text-info border-info/30" },
direction_approved: { label: "כיוון אושר", cls: "bg-info-bg text-info border-info/30" },
qa_review: { label: "בדיקת-איכות", cls: "bg-gold-wash text-gold-deep border-gold/40" },
drafted: { label: "טיוטה", cls: "bg-gold-wash text-gold-deep border-gold/40" },
exported: { label: "יוצא", cls: "bg-success-bg text-success border-success/40" },
reviewed: { label: "נסקר", cls: "bg-success-bg text-success border-success/40" },
final: { label: "סופי", cls: "bg-success-bg text-success border-success/40" },
};
function StatusChip({ status }: { status?: CaseStatus }) {
const c = (status && STATUS_CHIP[status]) || {
label: "בעריכה",
cls: "bg-info-bg text-info border-info/30",
};
return (
<span
className={`rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border ${c.cls}`}
>
{c.label}
</span>
);
}
function subtypeLabel(subtype?: string | null): string | null {
if (!subtype) return null;
return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null;
@@ -289,7 +268,9 @@ export default function ComposePage({
</Link>
<div className="flex items-center gap-3 flex-wrap">
<h1 className="text-navy text-2xl font-bold mb-0">ערר {caseNumber}</h1>
<StatusChip status={caseQuery.data?.status} />
{/* read-only status chip — reuses the shared StatusBadge (G2: no
parallel status map). Status is changed only in the overview hero. */}
{caseQuery.data?.status && <StatusBadge status={caseQuery.data.status} />}
{subtype && (
<span className="rounded-full text-[0.78rem] font-semibold px-3 py-0.5 border border-rule bg-gold-wash text-gold-deep">
{subtype}
@@ -313,10 +294,9 @@ export default function ComposePage({
</CardContent>
</Card>
) : (
/* ── Two-column workspace: tabbed main editor + 320px side rail ──── */
<div className="grid gap-6 lg:grid-cols-[1fr_320px] items-start">
{/* MAIN — block editor (default) + chair positions, as tabs (mockup 03) */}
<div className="min-w-0">
/* ── Full-width workspace (mockup 03b): no side rail; the case-docs
table is gone (DocumentsPanel in the overview is the sole owner),
relocated here as a collapsible doc-strip inside the positions tab. ── */
<Tabs defaultValue="blocks" dir="rtl">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="blocks">עורך הבלוקים</TabsTrigger>
@@ -335,7 +315,51 @@ export default function ComposePage({
</TabsContent>
{/* Tab 2 — chair positions on the analyst's threshold-claims + issues */}
<TabsContent value="positions" className="mt-5">
<TabsContent value="positions" className="mt-5 space-y-4">
{/* ★ collapsible doc-strip at the top, closed by default (mockup 03b).
Read-only pointer to the docs owned by the overview tab. */}
<Accordion type="single" collapsible>
<AccordionItem
value="docs"
className="overflow-hidden rounded-lg border border-rule bg-surface shadow-sm"
>
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<span className="flex flex-1 items-center gap-2.5">
<FileText className="size-3.5 text-gold-deep" aria-hidden />
<span className="text-navy text-[0.85rem] font-semibold">מסמכי התיק</span>
<span className="text-ink-muted text-[0.78rem]">
{documents.length} מסמכים
</span>
</span>
</AccordionTrigger>
<AccordionContent className="px-4 pb-3.5 pt-0">
{documents.length === 0 ? (
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
) : (
<div className="flex flex-wrap gap-2 border-t border-rule-soft pt-3">
{documents.map((d) => (
<span
key={d.id}
className="inline-flex items-center gap-1.5 rounded-full border border-rule bg-parchment px-3 py-1 text-[0.78rem] text-ink-soft"
title={d.title}
>
<FileText className="size-3 text-gold-deep shrink-0" aria-hidden />
<span className="truncate max-w-[14rem]">{d.title || "מסמך"}</span>
<span className="rounded bg-gold-wash text-gold-deep text-[0.66rem] font-semibold px-1.5 py-0.5">
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
</span>
</span>
))}
</div>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
<p className="text-ink-muted text-[0.78rem]">
סוגיות-המחלוקת ועמדות הצדדים. עורך עמדת-היו״ר נשמר אוטומטית ומזין את בלוק י׳ (דיון והכרעה).
</p>
{analysis.isPending ? (
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-3">
@@ -365,7 +389,7 @@ export default function ComposePage({
</Card>
) : analysis.data ? (
<div className="space-y-6">
{/* Threshold claims */}
{/* Threshold claims — laid out 2-up across the full width (mockup 03b) */}
{analysis.data.threshold_claims &&
analysis.data.threshold_claims.length > 0 && (
<div className="space-y-3">
@@ -375,7 +399,7 @@ export default function ComposePage({
{analysis.data.threshold_claims.length}
</span>
</div>
<div className="space-y-2.5">
<div className="grid gap-3 lg:grid-cols-2 items-start">
{analysis.data.threshold_claims.map((tc) => (
<SubsectionCard
key={tc.id}
@@ -389,7 +413,7 @@ export default function ComposePage({
</div>
)}
{/* Issues */}
{/* Issues — 2-up grid */}
{analysis.data.issues && analysis.data.issues.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2">
@@ -398,7 +422,7 @@ export default function ComposePage({
{analysis.data.issues.length}
</span>
</div>
<div className="space-y-2.5">
<div className="grid gap-3 lg:grid-cols-2 items-start">
{analysis.data.issues.map((iss) => (
<SubsectionCard
key={iss.id}
@@ -442,42 +466,13 @@ export default function ComposePage({
)}
</div>
) : null}
</TabsContent>
</Tabs>
</div>
{/* SIDE RAIL — documents · attached precedents · finish-and-transfer */}
<aside className="space-y-4 lg:sticky lg:top-4">
{/* מסמכי התיק */}
{/* ── case-level פסיקה מצורפת + finish/export gates — relocated from
the removed side rail; kept inline (INV-IA3: gates not removed). ── */}
<div className="grid gap-4 lg:grid-cols-2 items-start pt-2">
{/* פסיקה מצורפת (case-level) — stays inline */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-4 py-4">
<h3 className="text-navy text-[0.9rem] font-semibold mb-2">מסמכי התיק</h3>
{documents.length === 0 ? (
<p className="text-[0.78rem] text-ink-muted">אין מסמכים מצורפים</p>
) : (
<ul>
{documents.map((d) => (
<li
key={d.id}
className="flex items-center gap-2 text-[0.82rem] text-ink-soft py-1.5 border-b border-rule-soft last:border-0"
>
<FileText className="w-3.5 h-3.5 text-ink-muted shrink-0" aria-hidden />
<span className="truncate flex-1" title={d.title}>
{d.title || "מסמך"}
</span>
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-1.5 py-0.5 shrink-0 whitespace-nowrap">
{DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
{/* פסיקה מצורפת (case-level) */}
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-4 py-4">
<CardContent className="px-5 py-4">
<h3 className="text-navy text-[0.9rem] font-semibold mb-1">פסיקה מצורפת</h3>
<p className="text-[0.72rem] text-ink-muted mb-3">
ציטוטים התומכים בעמדה באופן רוחבי ישולבו בפתיחת בלוק י (דיון).
@@ -492,14 +487,15 @@ export default function ComposePage({
</CardContent>
</Card>
{/* השלמה והעברה */}
{/* השלמה והעברה — export/upload gates, relocated not removed */}
<FinishRail
caseNumber={caseNumber}
hasAnalysis={!!analysis.data}
onUploaded={() => analysis.refetch()}
/>
</aside>
</div>
</TabsContent>
</Tabs>
)}
</AppShell>
);

View File

@@ -9,26 +9,18 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { CaseHeader } from "@/components/cases/case-header";
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
import { WorkflowTimeline } from "@/components/cases/workflow-timeline";
import { StatusGuide } from "@/components/cases/status-guide";
import { StatusChanger } from "@/components/cases/status-changer";
import { StatusHero } from "@/components/cases/status-hero";
import { DocumentsPanel } from "@/components/cases/documents-panel";
import { DraftsPanel } from "@/components/cases/drafts-panel";
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
import { LegalArgumentsPanel } from "@/components/cases/legal-arguments-panel";
import { AgentActivityFeed } from "@/components/cases/agent-activity-feed";
import { AgentActivityPreview } from "@/components/cases/agent-activity-preview";
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
import { UploadSheet } from "@/components/documents/upload-sheet";
import { expectedOutcomes } from "@/lib/schemas/case";
import { useCase, useStartWorkflow } from "@/lib/api/cases";
import { toast } from "sonner";
import { Play, Loader2 } from "lucide-react";
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
expectedOutcomes.map((o) => [o.value, o.label]),
);
/*
* Next 16 breaking change: route params are now a Promise.
* The `use()` hook unwraps them inside a client component.
@@ -42,9 +34,6 @@ export default function CaseDetailPage({
const { data, isPending, error, refetch } = useCase(caseNumber);
const startWorkflow = useStartWorkflow(caseNumber);
const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready";
const expectedOutcomeLabel = data?.expected_outcome
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
: null;
// Only take over the whole page when there is NO data to show. A transient
// 5xx on the 5s background refetch must not blow away an already-loaded page.
@@ -138,13 +127,18 @@ export default function CaseDetailPage({
<CaseHeader data={data} actions={bandActions} tabs={tabsList} />
)}
{/* two-column wrap — main tab content (1fr) + rail (340px) */}
<div className="grid gap-6 lg:grid-cols-[1fr_340px] items-start mt-6">
<div className="min-w-0">
<TabsContent value="overview" className="mt-0 space-y-5">
<DocumentsPanel data={data} />
{/* full-width tab content — status lives ONLY in the overview hero
(mockup 18b); the other tabs lose the status rail entirely (INV-IA1). */}
<div className="min-w-0 mt-6">
<TabsContent value="overview" className="mt-0 space-y-6">
{/* ★ STATUS HERO — the single home for case status (INV-IA1/INV-UI7) */}
<StatusHero caseNumber={caseNumber} data={data} />
{/* documents + agents two-column below the hero (mockup 18b) */}
<div className="grid gap-6 lg:grid-cols-2 items-start">
<DocumentsPanel data={data} />
<AgentActivityPreview caseNumber={caseNumber} />
</div>
{/* decision-editor CTA moved to the band actions (visible on all tabs) */}
</TabsContent>
@@ -180,32 +174,6 @@ export default function CaseDetailPage({
</Card>
</TabsContent>
</div>
{/* rail — status timeline + status controls (mockup .rail) */}
<div className="space-y-5">
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0 h-fit">
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
סטטוס התיק
</div>
<CardContent className="px-5 py-4 space-y-4">
<AgentStatusWidget caseNumber={caseNumber} />
<WorkflowTimeline status={data?.status} />
{expectedOutcomeLabel && (
<div className="border-t border-rule-soft pt-3">
<dl className="flex justify-between items-center text-sm">
<dt className="text-ink-muted">תוצאה צפויה</dt>
<dd className="rounded-full bg-warn-bg text-warn text-[0.75rem] font-semibold px-3 py-0.5">
{expectedOutcomeLabel}
</dd>
</dl>
</div>
)}
<StatusChanger caseNumber={caseNumber} currentStatus={data?.status} />
<StatusGuide />
</CardContent>
</Card>
</div>
</div>
</Tabs>
</AppShell>
);

View File

@@ -36,6 +36,23 @@ const PRIORITY_BADGE_TONE: Record<LegalArgumentPriority, string> = {
relief: "bg-emerald-50 text-emerald-900 border-emerald-200",
};
// Per-party colour mark + one-line role description (mockup 25b party trigger).
const PARTY_MARK_TONE: Record<LegalArgumentParty, string> = {
appellant: "bg-info",
respondent: "bg-gold-deep",
committee: "bg-success",
permit_applicant: "bg-warn",
unknown: "bg-ink-muted",
};
const PARTY_SUB_HE: Record<LegalArgumentParty, string> = {
appellant: "הצד היוזם של הערר",
respondent: "הצד המשיב לערר",
committee: "עמדת הגורם המאשר",
permit_applicant: "צד שלישי שזכותו עשויה להיפגע",
unknown: "צד שלא זוהה אוטומטית",
};
function groupByPriority(
args: LegalArgument[],
): Record<LegalArgumentPriority, LegalArgument[]> {
@@ -55,22 +72,18 @@ function groupByPriority(
}
type PartySectionProps = {
party: LegalArgumentParty;
args: LegalArgument[];
};
function PartySection({ party, args }: PartySectionProps) {
/**
* Inner body of a single party — the priority-grouping + per-argument inner
* accordion. The party header (name / sub / count) now lives on the enclosing
* party-accordion trigger (mockup 25b), so this renders the groups only.
*/
function PartySection({ args }: PartySectionProps) {
const grouped = useMemo(() => groupByPriority(args), [args]);
return (
<div className="space-y-3">
<div className="flex items-baseline justify-between border-b border-rule pb-2">
<h3 className="text-navy text-base font-semibold">
{PARTY_LABELS_HE[party] ?? party}
</h3>
<span className="text-ink-muted text-xs">
{args.length} טיעונים
</span>
</div>
<div className="space-y-4">
{PRIORITY_ORDER.map((priority) => {
const list = grouped[priority];
if (!list?.length) return null;
@@ -253,15 +266,47 @@ export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) {
החישוב למנתח המשפטי.
</p>
) : (
<div className="space-y-6">
{parties.map((party) => (
<PartySection
// Top-level party accordion — each party is its own collapsible section,
// closed by default (empty defaultValue). The count badge on every
// trigger shows the scope without expanding (mockup 25b).
<Accordion type="multiple" className="space-y-3.5">
{parties.map((party) => {
const args = data.by_party[party] ?? [];
return (
<AccordionItem
key={party}
party={party}
args={data.by_party[party] ?? []}
value={party}
className="overflow-hidden rounded-lg border border-rule bg-surface shadow-sm"
>
<AccordionTrigger className="px-5 py-4 hover:no-underline data-[state=open]:border-b data-[state=open]:border-rule-soft data-[state=open]:bg-parchment">
<span className="flex flex-1 items-center gap-3.5">
<span
className={`h-8 w-1.5 flex-none rounded ${PARTY_MARK_TONE[party]}`}
aria-hidden
/>
))}
</div>
<span className="flex flex-1 flex-col items-start gap-0.5">
<span className="text-navy text-base font-bold leading-tight">
{PARTY_LABELS_HE[party] ?? party}
</span>
<span className="text-ink-muted text-xs">
{PARTY_SUB_HE[party]}
</span>
</span>
<Badge
variant="outline"
className="bg-gold-wash text-gold-deep border-rule rounded-full px-3 py-0.5 text-xs font-bold whitespace-nowrap"
>
{args.length} טיעונים
</Badge>
</span>
</AccordionTrigger>
<AccordionContent className="px-5 pb-4 pt-4">
<PartySection args={args} />
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
)}
</div>
);

View File

@@ -21,9 +21,13 @@ const ALL_STATUSES: readonly CaseStatus[] = CASE_STATUSES;
export function StatusChanger({
caseNumber,
currentStatus,
inline = false,
}: {
caseNumber: string;
currentStatus?: CaseStatus;
/** Compact horizontal layout for the status hero (mockup 18b hero-controls);
* default is the stacked rail layout. */
inline?: boolean;
}) {
// `null` = untouched → the dropdown tracks the live `currentStatus` (which
// arrives async and changes on the 5s poll / external updates). Only an
@@ -45,8 +49,10 @@ export function StatusChanger({
};
return (
<div className="mt-4 border-t border-rule pt-3 space-y-2">
<div className={inline ? "space-y-0" : "mt-4 border-t border-rule pt-3 space-y-2"}>
{!inline && (
<label className="text-[0.72rem] text-ink-muted block">שינוי סטטוס ידני</label>
)}
<div className="flex items-center gap-2">
<Select
value={effective || "__current__"}
@@ -77,7 +83,7 @@ export function StatusChanger({
disabled={!canSave || mutate.isPending}
onClick={handleSave}
>
{mutate.isPending ? "שומר…" : "עדכן"}
{mutate.isPending ? "שומר…" : inline ? "עדכן סטטוס" : "עדכן"}
</Button>
</div>
</div>

View File

@@ -0,0 +1,170 @@
"use client";
import { PHASES, type CaseStatus } from "@/lib/api/case-status";
import { statusLabel } from "@/lib/api/case-status";
import { StatusChanger } from "@/components/cases/status-changer";
import { StatusGuide } from "@/components/cases/status-guide";
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
import { expectedOutcomes } from "@/lib/schemas/case";
import type { CaseDetail } from "@/lib/api/cases";
/*
* StatusHero — the single, prominent home for case status (mockup 18b, INV-IA1).
* Renders the horizontal pipeline stepper, the current-stage chip, the manual
* status changer, and a meta strip. This is the ONLY surface in the app that
* owns the status changer / pipeline timeline (status was removed from the
* compose editor and the other case tabs). Reuses the shared StatusChanger,
* StatusGuide and AgentStatusWidget so no parallel status path is created (G2).
*/
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
expectedOutcomes.map((o) => [o.value, o.label]),
);
function formatDate(iso?: string | null): string {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso ?? "—";
}
}
function phaseIndexOf(status?: CaseStatus): number {
if (!status) return -1;
return PHASES.findIndex((p) => p.statuses.includes(status));
}
/** Horizontal RTL stepper over the 5 lifecycle phases (mockup 18b .stepper). */
function HorizontalStepper({ status }: { status?: CaseStatus }) {
const currentIdx = phaseIndexOf(status);
return (
<ol className="flex items-start gap-0">
{PHASES.map((phase, i) => {
const state =
currentIdx === -1 ? "pending"
: i < currentIdx ? "done"
: i === currentIdx ? "now"
: "pending";
const isLast = i === PHASES.length - 1;
const knobTone =
state === "done"
? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
: state === "now"
? "bg-gold [box-shadow:0_0_0_2px_var(--color-gold-wash),0_0_0_3px_var(--color-gold)]"
: "bg-rule [box-shadow:0_0_0_1px_var(--color-rule)]";
const labelTone =
state === "done"
? "text-ink-soft"
: state === "now"
? "text-navy font-bold"
: "text-ink-muted";
// connector runs toward the next (earlier-completed) phase; in RTL the
// line sits to the start side. Coloured when this phase is done.
const segTone = state === "done" ? "bg-success" : "bg-rule";
return (
<li
key={phase.key}
className="relative flex-1 pt-7 text-center"
>
{!isLast && (
<span
className={`absolute top-[9px] h-0.5 w-full ${segTone}`}
style={{ insetInlineStart: "50%" }}
aria-hidden
/>
)}
<span
className={`absolute top-[3px] h-4 w-4 -translate-x-1/2 rounded-full border-[3px] border-surface ${knobTone}`}
style={{ insetInlineStart: "50%", transform: "translateX(50%)" }}
aria-hidden
/>
<span className={`block text-[0.78rem] ${labelTone}`}>
{phase.label}
<span className="block text-[0.68rem] text-ink-muted mt-0.5 tabular-nums">
{state === "now" ? "כעת" : state === "done" ? "✓" : "—"}
</span>
</span>
</li>
);
})}
</ol>
);
}
export function StatusHero({
caseNumber,
data,
}: {
caseNumber: string;
data?: CaseDetail;
}) {
const status = data?.status;
const expectedOutcomeLabel = data?.expected_outcome
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
: null;
return (
<div className="rounded-xl border border-rule bg-surface shadow-sm px-6 py-5">
{/* hero top — title + current stage chip + manual changer */}
<div className="flex items-start justify-between flex-wrap gap-4 mb-4">
<div className="flex items-center gap-3">
<h2 className="text-navy text-base font-bold">סטטוס התיק</h2>
{status && (
<span className="rounded-full bg-warn-bg text-warn text-[0.8rem] font-bold px-3.5 py-1">
כעת: {statusLabel(status)}
</span>
)}
</div>
{/* manual status changer — the SINGLE manual-status surface (INV-IA1) */}
<StatusChanger caseNumber={caseNumber} currentStatus={status} inline />
</div>
{/* horizontal pipeline */}
<HorizontalStepper status={status} />
{/* meta strip below the stepper (mockup 18b .hero-meta) */}
<div className="flex flex-wrap gap-x-8 gap-y-3 mt-5 pt-4 border-t border-rule-soft">
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">תוצאה צפויה</span>
<span className="text-sm font-semibold mt-0.5">
{expectedOutcomeLabel ? (
<span className="inline-block rounded-full bg-warn-bg text-warn text-[0.78rem] font-semibold px-2.5 py-0.5">
{expectedOutcomeLabel}
</span>
) : (
<span className="text-ink-soft">לא נקבע</span>
)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">תאריך דיון</span>
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
{formatDate(data?.hearing_date)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">עודכן לאחרונה</span>
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
{formatDate(data?.updated_at)}
</span>
</div>
</div>
{/* agents activity + status-map progressive disclosure (INV-IA6) */}
<div className="mt-4 pt-4 border-t border-rule-soft space-y-3">
<AgentStatusWidget caseNumber={caseNumber} />
<StatusGuide />
</div>
</div>
);
}