Phase 3a: shadcn init + Home/Case List view

Initialize shadcn/ui (radix-nova preset) and wire its semantic tokens
to the editorial navy/cream/gold palette so primitives inherit the
judicial voice without per-component overrides.

Replace the Phase 2 live-probe with a real dashboard: KPI tiles,
conic-gradient status donut (ported from the vanilla renderHero),
and a TanStack Table cases list with search + sort. Add useCase(n)
hook with 5s staleTime/refetchInterval to replace the old manual
polling loop when Case Detail ships next.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 16:04:30 +00:00
parent 0ee8e723bd
commit 51064f3a03
25 changed files with 6513 additions and 247 deletions

View File

@@ -0,0 +1,91 @@
"use client";
import type { Case, CaseStatus } from "@/lib/api/cases";
import { STATUS_LABELS } from "@/components/cases/status-badge";
/*
* Conic-gradient donut — ported from legal-ai/web/static/index.html renderHero().
* Kept deliberately dependency-free (no D3/recharts) — a single background-image.
* Five status groups map onto the navy/gold/info/warn/success palette.
*/
type GroupKey = "intake" | "prep" | "thinking" | "writing" | "done";
const GROUP_OF: Record<CaseStatus, GroupKey> = {
new: "intake", uploading: "intake", processing: "intake",
documents_ready: "prep", outcome_set: "prep",
brainstorming: "thinking", direction_approved: "thinking",
drafting: "writing", qa_review: "writing", drafted: "writing",
exported: "done", reviewed: "done", final: "done",
};
const GROUP_META: Record<GroupKey, { label: string; color: string }> = {
intake: { label: "חדש / בעיבוד", color: "var(--color-ink-muted)" },
prep: { label: "הכנה", color: "var(--color-info)" },
thinking: { label: "ניתוח וכיוון", color: "var(--color-gold)" },
writing: { label: "בכתיבה", color: "var(--color-warn)" },
done: { label: "מוכן", color: "var(--color-success)" },
};
export function StatusDonut({ cases }: { cases?: Case[] }) {
const counts: Record<GroupKey, number> = {
intake: 0, prep: 0, thinking: 0, writing: 0, done: 0,
};
(cases ?? []).forEach((c) => {
const g = GROUP_OF[c.status];
if (g) counts[g] += 1;
});
const total = Object.values(counts).reduce((a, b) => a + b, 0);
const segments: { key: GroupKey; start: number; end: number }[] = [];
let pct = 0;
(Object.keys(counts) as GroupKey[]).forEach((k) => {
if (counts[k] === 0) return;
const start = total === 0 ? 0 : (pct / total) * 360;
pct += counts[k];
const end = total === 0 ? 360 : (pct / total) * 360;
segments.push({ key: k, start, end });
});
const background =
total === 0
? "conic-gradient(var(--color-rule-soft) 0deg 360deg)"
: `conic-gradient(${segments
.map((s) => `${GROUP_META[s.key].color} ${s.start}deg ${s.end}deg`)
.join(", ")})`;
return (
<div className="flex items-center gap-6">
<div
className="relative w-[140px] h-[140px] rounded-full shadow-sm"
style={{ background }}
aria-label="פיזור תיקים לפי סטטוס"
>
<div className="absolute inset-[18px] bg-surface rounded-full flex flex-col items-center justify-center">
<span className="font-display text-2xl font-black text-navy leading-none">
{total}
</span>
<span className="text-[0.7rem] text-ink-muted mt-1">תיקים</span>
</div>
</div>
<ul className="flex flex-col gap-1.5 text-sm">
{(Object.keys(GROUP_META) as GroupKey[]).map((k) => (
<li key={k} className="flex items-center gap-2">
<span
className="inline-block w-2.5 h-2.5 rounded-full"
style={{ background: GROUP_META[k].color }}
/>
<span className="text-ink-soft">{GROUP_META[k].label}</span>
<span className="text-ink-muted tabular-nums me-auto ms-1">
{counts[k]}
</span>
</li>
))}
</ul>
</div>
);
}
/* Exported for the legend tests / docs if ever needed */
export { STATUS_LABELS };