feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (Claude Design) על כל הדפים — שינוי-הרכב אמיתי פר-מוקאפ, לא ליטוש-טוקנים. כל hook/query/mutation/טאב/ טופס/נתון נשמר (אומת: tsc נקי + בדיקת-נוכחות hooks קריטיים; 0 פונקציונליות נמחקה). דפים (← מוקאפ): - בית — לוח: KPI + "תיקים לפי סטטוס" (bars) + כרטיס-אישורים + CTA כפול. - ארכיון — filter-bar שטוח + טבלה נקייה + צ'יפי-סוג/תוצאה. - הערות יו״ר — פריסה דו-טורית + טופס-הוספה חי + כרטיסי-הערה. - ספריית-פסיקה — tabs קו-תחתון + כרטיסי-תוצאה halacha/קטע + AuthorityBadge. - דף-תקדים — באנר-meta parchment + דו-טורי + provenance pills. - פסיקה-חסרה — pill פתוחים + צ'יפי-סטטוס + CTA העלאה. - יומונים — אזור-העלאה מקווקו + כרטיסי-digest + "ממתין" כתווית פסיבית. - גרף — פאנל-צד שכבות/אנליטיקה + canvas parchment. - אימון-סגנון — פורטרט: banner + KPI + אנטומיה + ביטויי-חתימה. - מתודולוגיה — עורך-צ'קליסט + "חל על:" + canon chip. - מיומנויות/סקריפטים — טבלאות אמיתיות + צ'יפי-סטטוס. - הגדרות — sidenav דו-טורי + env-rows עם "ממתין ל-redeploy". - דף-תיק — באנר-תיק parchment + tabs + timeline + "פתח עורך החלטה". - תפעול — SectionHeaders + טבלת-שירותים + כרטיסי-שער gold-wash. - compose — באנר-תיק + SOT pill + פריסה דו-טורית + "השלמה והעברה". תיקונים שלי אחרי הסוכנים: documents-panel (הוצאת רכיב Shell מ-render — React Compiler), scripts useMemo deps. /approvals כבר נבנה מחדש נאמנה (commit קודם). בדיקות: npx tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש). שימור-פונקציונליות אומת. CI Docker build = שער סופי לפני deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { StatusBadge } from "@/components/cases/status-badge";
|
||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||
@@ -25,87 +25,129 @@ function formatDate(iso?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
export function CaseHeader({ data }: { data?: CaseDetail }) {
|
||||
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 (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<div className="flex items-start justify-between gap-6 flex-wrap">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<span className="font-display text-[2rem] font-black text-navy leading-none 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-2.5 py-0.5 text-[0.72rem] font-medium bg-ink-muted/10 text-ink-muted border-ink-muted/30"
|
||||
>
|
||||
בארכיון
|
||||
</Badge>
|
||||
)}
|
||||
{data?.practice_area && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-medium bg-gold-wash text-gold-deep border-gold/40"
|
||||
>
|
||||
{PRACTICE_AREA_LABELS[data.practice_area]}
|
||||
{data.appeal_subtype && data.appeal_subtype !== "unknown" && (
|
||||
<> · {APPEAL_SUBTYPE_LABELS[data.appeal_subtype]}</>
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{(data?.proceeding_type === 'בל"מ' || isBlamSubtype(data?.appeal_subtype)) && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="rounded-full px-2.5 py-0.5 text-[0.72rem] font-bold bg-warn/10 text-warn-deep border-warn/40"
|
||||
title="בקשה להארכת מועד להגשת ערר"
|
||||
>
|
||||
בל"מ
|
||||
</Badge>
|
||||
)}
|
||||
{data?.case_number && (
|
||||
<CaseArchiveAction
|
||||
caseNumber={data.case_number}
|
||||
archivedAt={data.archived_at}
|
||||
/>
|
||||
)}
|
||||
<CreateRepoButton data={data} />
|
||||
</div>
|
||||
<h1 className="text-navy text-xl font-bold leading-snug max-w-2xl mb-0">
|
||||
{data?.title ?? "טוען…"}
|
||||
</h1>
|
||||
{data?.subject && (
|
||||
<p className="text-ink-muted text-sm max-w-2xl leading-relaxed">
|
||||
{data.subject}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-6 flex-wrap">
|
||||
<div className="min-w-0">
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
{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="בקשה להארכת מועד להגשת ערר"
|
||||
>
|
||||
בל"מ
|
||||
</Badge>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||
<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>
|
||||
{/* 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) */}
|
||||
{parties ? (
|
||||
<p className="text-ink-soft text-sm mt-1.5">{parties}</p>
|
||||
) : data?.subject ? (
|
||||
<p className="text-ink-soft text-sm mt-1.5 max-w-3xl leading-relaxed">
|
||||
{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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import {
|
||||
@@ -295,6 +295,25 @@ function DocumentRow({
|
||||
|
||||
/* ── Main panel ────────────────────────────────────────────────── */
|
||||
|
||||
// IA-redesign mockup 17 — card with a parchment header band wrapping the
|
||||
// (unchanged) document list. Module-level so it isn't re-created during render
|
||||
// (React Compiler: "Cannot create components during render").
|
||||
function DocumentsShell({ count, children }: { count: number; children: ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||
<div className="px-5 py-3.5 border-b border-rule-soft bg-parchment text-[0.92rem] font-semibold text-navy">
|
||||
מסמכי התיק
|
||||
{count > 0 && (
|
||||
<span className="ms-2 text-[0.72rem] text-ink-muted font-medium tabular-nums">
|
||||
({count})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-5 py-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocumentsPanel({
|
||||
data,
|
||||
}: {
|
||||
@@ -305,10 +324,12 @@ export function DocumentsPanel({
|
||||
|
||||
if (docs.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-ink-muted">
|
||||
<div className="text-gold text-2xl mb-2" aria-hidden="true">❦</div>
|
||||
<p className="text-sm">אין מסמכים בתיק זה</p>
|
||||
</div>
|
||||
<DocumentsShell count={docs.length}>
|
||||
<div className="text-center py-12 text-ink-muted">
|
||||
<div className="text-gold text-2xl mb-2" aria-hidden="true">❦</div>
|
||||
<p className="text-sm">אין מסמכים בתיק זה</p>
|
||||
</div>
|
||||
</DocumentsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,7 +349,8 @@ export function DocumentsPanel({
|
||||
const pct = docs.length > 0 ? Math.round((done / docs.length) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<DocumentsShell count={docs.length}>
|
||||
<div className="space-y-3">
|
||||
{hasIncomplete && (
|
||||
<div className="rounded-lg border border-rule bg-parchment/40 px-4 py-3 space-y-2" dir="rtl">
|
||||
<div className="flex items-center gap-4 text-[0.78rem] flex-wrap">
|
||||
@@ -371,6 +393,7 @@ export function DocumentsPanel({
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentsShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,11 +38,7 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
const currentIdx = phaseIndexOf(status);
|
||||
|
||||
return (
|
||||
<ol className="relative space-y-4">
|
||||
<div
|
||||
className="absolute top-2 bottom-2 right-[11px] w-px bg-rule"
|
||||
aria-hidden
|
||||
/>
|
||||
<ol className="relative">
|
||||
{PHASES.map((phase, i) => {
|
||||
const state =
|
||||
currentIdx === -1 ? "pending"
|
||||
@@ -51,9 +47,9 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
: "pending";
|
||||
|
||||
const dotTone =
|
||||
state === "done" ? "bg-success border-success"
|
||||
: state === "current" ? "bg-gold border-gold shadow-[0_0_0_4px_color-mix(in_oklab,var(--color-gold)_20%,transparent)]"
|
||||
: "bg-surface border-rule";
|
||||
state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
||||
: state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
|
||||
: "bg-rule";
|
||||
|
||||
const labelTone =
|
||||
state === "done" ? "text-ink-soft"
|
||||
@@ -66,34 +62,55 @@ export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
||||
: "text-ink-muted/50";
|
||||
|
||||
const PhaseIcon = phase.icon;
|
||||
const StatusIcon = status ? STATUS_ICONS[status] : null;
|
||||
const isLast = i === PHASES.length - 1;
|
||||
|
||||
return (
|
||||
<li key={phase.key} className="relative flex items-start gap-3 ps-7">
|
||||
<li
|
||||
key={phase.key}
|
||||
className="relative flex items-center gap-3 py-2"
|
||||
>
|
||||
{/* connector line below the dot (mockup .tl .line) */}
|
||||
{!isLast && (
|
||||
<span
|
||||
className="absolute top-[26px] w-px h-[20px] bg-rule"
|
||||
style={{ insetInlineStart: "5px" }}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`absolute right-[5px] top-1 inline-block w-3 h-3 rounded-full border-2 ${dotTone}`}
|
||||
className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className={`text-sm flex items-center gap-1.5 ${labelTone}`}>
|
||||
<div className="flex items-center gap-2 grow min-w-0">
|
||||
<span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
|
||||
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
|
||||
{phase.label}
|
||||
</span>
|
||||
{state === "current" && status && (
|
||||
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
||||
{StatusIcon && <StatusIcon className="w-3 h-3 shrink-0" />}
|
||||
{STATUS_LABELS[status]}
|
||||
</span>
|
||||
)}
|
||||
{state === "current" && status && STATUS_DESCRIPTIONS[status] && (
|
||||
<span className="text-[0.65rem] text-ink-muted leading-snug mt-0.5">
|
||||
{STATUS_DESCRIPTIONS[status]}
|
||||
</span>
|
||||
)}
|
||||
<span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
|
||||
{state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{/* current micro-status detail under the active phase */}
|
||||
{currentIdx !== -1 && status && (
|
||||
<li className="ps-[26px] pt-1">
|
||||
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
||||
{STATUS_ICONS[status] &&
|
||||
(() => {
|
||||
const Icon = STATUS_ICONS[status];
|
||||
return <Icon className="w-3 h-3 shrink-0" />;
|
||||
})()}
|
||||
{STATUS_LABELS[status]}
|
||||
</span>
|
||||
{STATUS_DESCRIPTIONS[status] && (
|
||||
<span className="block text-[0.65rem] text-ink-muted leading-snug mt-0.5">
|
||||
{STATUS_DESCRIPTIONS[status]}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,26 +33,24 @@ export function DigestCard({
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
const linked = Boolean(digest.linked_case_law_id);
|
||||
const pending = digest.extraction_status !== "completed";
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
|
||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
{digest.concept_tag && (
|
||||
<Badge className="bg-gold text-navy border-0">{digest.concept_tag}</Badge>
|
||||
)}
|
||||
<div className="flex flex-col rounded-lg border border-rule bg-surface shadow-sm p-4">
|
||||
{/* top row — yomon-number + date at start, concept tag pushed to end */}
|
||||
<div className="flex items-center gap-2.5 mb-2.5 flex-wrap">
|
||||
{digest.yomon_number && (
|
||||
<span className="font-mono" dir="ltr">
|
||||
<span className="text-navy font-bold text-[0.95rem]">
|
||||
יומון {digest.yomon_number}
|
||||
</span>
|
||||
)}
|
||||
{digest.digest_date && (
|
||||
<span className="text-ink-muted text-[0.78rem]">· {formatDate(digest.digest_date)}</span>
|
||||
)}
|
||||
{digest.publication && digest.publication !== "כל יום" && (
|
||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
||||
{digest.publication}
|
||||
</Badge>
|
||||
)}
|
||||
{digest.digest_date && <span>· {formatDate(digest.digest_date)}</span>}
|
||||
{digest.practice_area && (
|
||||
<span>· {practiceAreaLabel(digest.practice_area)}</span>
|
||||
)}
|
||||
{digest.digest_kind === "announcement" && (
|
||||
<Badge variant="outline" className="bg-sky-50 text-sky-700 border-sky-300 text-[0.65rem]">
|
||||
עדכון
|
||||
@@ -63,47 +61,45 @@ export function DigestCard({
|
||||
מאמר
|
||||
</Badge>
|
||||
)}
|
||||
{digest.extraction_status !== "completed" && (
|
||||
<Badge variant="outline" className="bg-rule-soft text-ink-muted text-[0.65rem]">
|
||||
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
||||
</Badge>
|
||||
{digest.concept_tag && (
|
||||
<span className="ms-auto rounded-full bg-info-bg text-info text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||
{digest.concept_tag}
|
||||
</span>
|
||||
)}
|
||||
{typeof score === "number" && (
|
||||
<span className="ms-auto tabular-nums">דירוג {score.toFixed(2)}</span>
|
||||
<span className="ms-1 tabular-nums text-ink-muted text-[0.78rem]">
|
||||
דירוג {score.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* holding — the digest headline */}
|
||||
{digest.headline_holding && (
|
||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
||||
<p className="text-ink font-medium text-[0.92rem] leading-6 mb-2.5" dir="rtl">
|
||||
{digest.headline_holding}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* source ruling — "מקור:" + citation, start-bordered (mockup `.ruling`) */}
|
||||
<div className="border-s-2 border-rule ps-3 text-[0.8rem] text-ink-muted leading-6" dir="rtl">
|
||||
<b className="text-ink-soft font-semibold">מקור:</b>{" "}
|
||||
{digest.underlying_citation || "—"}
|
||||
</div>
|
||||
|
||||
{digest.summary && (
|
||||
<p className="text-ink-soft text-sm leading-relaxed" dir="rtl">
|
||||
<p className="text-ink-soft text-[0.82rem] leading-relaxed mt-2" dir="rtl">
|
||||
{digest.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-2 flex-wrap pt-1 border-t border-rule-soft mt-1">
|
||||
<span className="text-[0.72rem] text-ink-muted mt-1">פסק מקורי:</span>
|
||||
<span className="text-[0.82rem] text-ink font-mono flex-1 min-w-[200px]" dir="rtl">
|
||||
{digest.underlying_citation || "—"}
|
||||
{digest.practice_area && (
|
||||
<span className="text-ink-muted text-[0.72rem] mt-2">
|
||||
{practiceAreaLabel(digest.practice_area)}
|
||||
</span>
|
||||
{linked ? (
|
||||
<Link href={`/precedents/${digest.linked_case_law_id}`}>
|
||||
<Badge className="bg-emerald-100 text-emerald-800 border-emerald-300 hover:bg-emerald-200">
|
||||
מקושר לפסק ↗
|
||||
</Badge>
|
||||
</Link>
|
||||
) : (
|
||||
<Badge variant="outline" className="bg-amber-50 text-amber-700 border-amber-300">
|
||||
הפסק טרם בקורפוס
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{digest.subject_tags?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{digest.subject_tags.map((t) => (
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||
{t}
|
||||
@@ -112,7 +108,31 @@ export function DigestCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actions && <div className="flex items-center gap-2 pt-1">{actions}</div>}
|
||||
{/* foot — link-status chip + passive "ממתין לעיבוד" label (mockup `.foot`) */}
|
||||
<div className="mt-3 pt-3 border-t border-rule-soft flex items-center gap-2 flex-wrap">
|
||||
{linked ? (
|
||||
<Link href={`/precedents/${digest.linked_case_law_id}`}>
|
||||
<span className="rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-3 py-0.5 hover:opacity-90">
|
||||
מקושר לפסיקה ↗
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="rounded-full bg-rule-soft text-ink-muted text-[0.72rem] font-semibold px-3 py-0.5">
|
||||
לא מקושר
|
||||
</span>
|
||||
)}
|
||||
{pending && (
|
||||
/* passive status label — a dot + text, deliberately NOT a button */
|
||||
<span className="ms-auto inline-flex items-center gap-1.5 text-[0.72rem] font-medium text-ink-muted">
|
||||
<span className="h-[7px] w-[7px] rounded-full bg-warn/60" aria-hidden />
|
||||
{digest.extraction_status === "pending" ? "ממתין לעיבוד" : digest.extraction_status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actions && (
|
||||
<div className="flex items-center gap-2 pt-3">{actions}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import type { PracticeArea } from "@/lib/api/precedent-library";
|
||||
import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
||||
import { DigestCard } from "./digest-card";
|
||||
import { DigestUploadDialog } from "./digest-upload-dialog";
|
||||
|
||||
type LinkedFilter = "all" | "linked" | "unlinked";
|
||||
|
||||
@@ -96,9 +95,6 @@ export function DigestListPanel() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="ms-auto">
|
||||
<DigestUploadDialog />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
@@ -117,6 +113,8 @@ export function DigestListPanel() {
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.78rem] text-ink-muted">{data.count} יומונים</p>
|
||||
{/* two-column card grid (mockup 10 `.grid`) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{data.items.map((d) => (
|
||||
<DigestCard
|
||||
key={d.id}
|
||||
@@ -148,6 +146,7 @@ export function DigestListPanel() {
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PRACTICE_AREAS } from "@/components/precedents/practice-area";
|
||||
* via the MCP drainer ``digest_process_pending`` — so the toast tells the
|
||||
* user the digest is queued, not yet searchable.
|
||||
*/
|
||||
export function DigestUploadDialog() {
|
||||
export function DigestUploadDialog({ trigger }: { trigger?: React.ReactNode } = {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [yomonNumber, setYomonNumber] = useState("");
|
||||
@@ -71,10 +71,12 @@ export function DigestUploadDialog() {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="bg-navy text-parchment hover:bg-navy-soft">
|
||||
<Upload className="w-4 h-4 me-1" />
|
||||
העלאת יומון
|
||||
</Button>
|
||||
{trigger ?? (
|
||||
<Button className="bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||
<Upload className="w-4 h-4 me-1" />
|
||||
העלאת יומון
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent dir="rtl">
|
||||
<DialogHeader>
|
||||
@@ -140,7 +142,7 @@ export function DigestUploadDialog() {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={upload.isPending}
|
||||
className="bg-navy text-parchment hover:bg-navy-soft"
|
||||
className="bg-gold text-white hover:bg-gold-deep border-transparent"
|
||||
>
|
||||
{upload.isPending ? "מעלה…" : "העלה"}
|
||||
</Button>
|
||||
|
||||
@@ -88,8 +88,11 @@ export function GraphFilterPanel({
|
||||
facets?: GraphFacets;
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
||||
<Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
|
||||
<CardContent className="space-y-5 p-4">
|
||||
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||
פילטרים וסינון
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="graph-search" className="text-xs text-ink-muted">
|
||||
חיפוש פסיקה
|
||||
@@ -250,32 +253,50 @@ export function GraphFilterPanel({
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs text-ink-muted">סוגי נקודות</Label>
|
||||
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||
שכבות הגרף
|
||||
</div>
|
||||
<ToggleRow
|
||||
label="נקודות-נושא"
|
||||
swatch="#a97d3a"
|
||||
checked={controls.showTopics}
|
||||
onCheckedChange={(v) => onChange({ showTopics: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="נקודות-תחום"
|
||||
swatch="#4a7c59"
|
||||
checked={controls.showPracticeAreas}
|
||||
onCheckedChange={(v) => onChange({ showPracticeAreas: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="חוסרי מחקר (פסיקה חסרה)"
|
||||
swatch="#a54242"
|
||||
checked={controls.showGaps}
|
||||
onCheckedChange={(v) => onChange({ showGaps: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="יומונים (כל יום)"
|
||||
swatch="#b8894a"
|
||||
checked={controls.showDigests}
|
||||
onCheckedChange={(v) => onChange({ showDigests: v })}
|
||||
/>
|
||||
<ToggleRow
|
||||
label="הלכות"
|
||||
checked={controls.showHalachot}
|
||||
onCheckedChange={(v) => onChange({ showHalachot: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stage-2 gate — halacha layer is dense, gated by default (mockup 11) */}
|
||||
<div className="rounded-lg border border-gold bg-gold-wash p-3.5 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-navy m-0">שלב ב׳ — שכבת הלכות</h4>
|
||||
<Switch
|
||||
className="ms-auto"
|
||||
checked={controls.showHalachot}
|
||||
onCheckedChange={(v) => onChange({ showHalachot: v })}
|
||||
aria-label="הצגת שכבת ההלכות"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
|
||||
הפעלת שכבת ההלכות (1,454 צמתים). מגודרת כברירת-מחדל בשל הצפיפות —
|
||||
הדלקה מציגה את הקשרים הלכה←פסיקה.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -350,18 +371,28 @@ function ToggleRow({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
disabled,
|
||||
swatch,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onCheckedChange: (v: boolean) => void;
|
||||
disabled?: boolean;
|
||||
swatch?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink"}`}>
|
||||
<div className={`flex items-center gap-2.5 ${disabled ? "opacity-55" : ""}`}>
|
||||
{swatch ? (
|
||||
<span
|
||||
className="inline-block size-2.5 rounded-full shrink-0 ring-1 ring-black/10"
|
||||
style={{ backgroundColor: swatch }}
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
<span className={`text-sm ${disabled ? "text-ink-muted/50" : "text-ink-soft"}`}>
|
||||
{label}
|
||||
</span>
|
||||
<Switch
|
||||
className="ms-auto"
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -226,7 +226,7 @@ export function GraphView() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-3 text-xs text-ink-muted">
|
||||
<span>
|
||||
<span className="inline-flex items-center gap-2 rounded-full border border-rule bg-surface px-3 py-1 tabular-nums">
|
||||
{data ? `${data.nodes.length} נקודות · ${data.edges.length} קשרים` : "—"}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -248,12 +248,12 @@ export function GraphView() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 h-[calc(100vh-320px)] min-h-[560px]">
|
||||
<div className="flex gap-5 h-[calc(100vh-320px)] min-h-[560px] items-start">
|
||||
<GraphFilterPanel controls={controls} onChange={onChange} facets={facets} />
|
||||
|
||||
<div
|
||||
ref={canvasAreaRef}
|
||||
className="relative flex-1 rounded-lg border border-rule bg-surface overflow-hidden"
|
||||
className="relative flex-1 h-full rounded-lg border border-rule bg-gradient-to-b from-[#f3ecda] to-[#efe6cf] shadow-sm overflow-hidden"
|
||||
>
|
||||
{error ? (
|
||||
<div className="grid h-full place-items-center p-6 text-center">
|
||||
@@ -311,6 +311,12 @@ export function GraphView() {
|
||||
)}
|
||||
|
||||
<Legend colorBy={controls.colorBy} />
|
||||
|
||||
{data ? (
|
||||
<div className="absolute bottom-3 start-3 rounded-full bg-surface/70 backdrop-blur px-2.5 py-1 text-[0.72rem] text-ink-muted tabular-nums">
|
||||
{data.nodes.length} צמתים מוצגים
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{selectedNode ? (
|
||||
@@ -340,9 +346,16 @@ function RankingPanel({
|
||||
.sort((a, b) => (b.betweenness ?? 0) - (a.betweenness ?? 0))
|
||||
.slice(0, 12);
|
||||
|
||||
const communities = new Set(
|
||||
nodes.map((n) => n.community).filter((c) => c != null),
|
||||
).size;
|
||||
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm w-72 shrink-0 overflow-y-auto">
|
||||
<CardContent className="p-4">
|
||||
<Card className="bg-surface border-rule shadow-sm w-[300px] shrink-0 max-h-full overflow-y-auto">
|
||||
<CardContent className="p-4 space-y-4">
|
||||
<div className="text-[0.8rem] font-semibold text-navy border-b border-rule-soft pb-2">
|
||||
אנליטיקה
|
||||
</div>
|
||||
<Tabs defaultValue="pagerank">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="pagerank" className="flex-1">
|
||||
@@ -359,6 +372,12 @@ function RankingPanel({
|
||||
<RankList items={byBetweenness} metric="betweenness" onPick={onPick} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{communities > 0 ? (
|
||||
<div className="flex items-center gap-2 border-t border-rule-soft pt-3 text-sm text-ink-soft">
|
||||
אשכולות:
|
||||
<b className="text-navy text-lg tabular-nums">{communities}</b>
|
||||
</div>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -377,18 +396,19 @@ function RankList({
|
||||
return <p className="text-ink-muted text-xs mt-3">אין נתונים.</p>;
|
||||
}
|
||||
return (
|
||||
<ol className="mt-2 space-y-1">
|
||||
<ol className="mt-2">
|
||||
{items.map((n, i) => (
|
||||
<li key={n.id}>
|
||||
<li key={n.id} className="border-b border-rule-soft last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPick(n)}
|
||||
className="flex w-full items-baseline justify-between gap-2 rounded px-2 py-1 text-start text-sm hover:bg-gold-wash"
|
||||
className="flex w-full items-baseline gap-2 px-1 py-1.5 text-start text-sm hover:bg-gold-wash rounded"
|
||||
>
|
||||
<span className="truncate">
|
||||
<span className="text-ink-muted text-xs">{i + 1}.</span> {n.label}
|
||||
<span className="w-4 shrink-0 text-ink-muted text-xs tabular-nums">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="text-ink-muted text-xs tabular-nums shrink-0">
|
||||
<span className="truncate text-ink-soft">{n.label}</span>
|
||||
<span className="ms-auto text-gold-deep font-semibold text-xs tabular-nums shrink-0">
|
||||
{((n[metric] ?? 0) * 100).toFixed(0)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -115,84 +115,124 @@ export function ContentChecklistsPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
const itemCount = current
|
||||
? current.draft.split("\n").filter((l) => /^\s*-\s*\[/.test(l)).length
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Tab selector */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{items.map((item) => (
|
||||
<Button
|
||||
key={item.key}
|
||||
size="sm"
|
||||
variant={active === item.key ? "default" : "outline"}
|
||||
onClick={() => { setActive(item.key); setPreview(false); }}
|
||||
className="text-xs"
|
||||
>
|
||||
{item.label}
|
||||
{item.isOverride && (
|
||||
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1">
|
||||
מותאם
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
<div className="space-y-[18px]">
|
||||
{/* Type buttons — gold active (mockup 13) */}
|
||||
<div className="flex gap-2.5 flex-wrap">
|
||||
{items.map((item) => {
|
||||
const isActive = active === item.key;
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActive(item.key);
|
||||
setPreview(false);
|
||||
}}
|
||||
className={
|
||||
isActive
|
||||
? "rounded-lg border border-gold bg-gold px-4 py-2 text-[0.84rem] font-semibold text-white"
|
||||
: "rounded-lg border border-rule bg-surface px-4 py-2 text-[0.84rem] font-medium text-ink-soft hover:border-gold/50"
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
{item.isOverride && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`text-[9px] ms-1.5 px-1 ${isActive ? "bg-white/20 text-white" : ""}`}
|
||||
>
|
||||
מותאם
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Editor / Preview */}
|
||||
{/* "חל על:" explainer band — gold-wash (mockup 13) */}
|
||||
{current && CHECKLIST_APPLIES[current.key] && (
|
||||
<div className="flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5 text-[0.84rem] text-ink-soft">
|
||||
<b className="text-gold-deep font-semibold whitespace-nowrap">חל על:</b>
|
||||
<span>{CHECKLIST_APPLIES[current.key]}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor — framed card with header chip + parchment editor + footer */}
|
||||
{current && (
|
||||
<Card className="border-rule">
|
||||
<CardContent className="px-5 py-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
||||
{CHECKLIST_APPLIES[current.key] && (
|
||||
<p className="text-[0.72rem] text-ink-muted mt-0.5">
|
||||
חל על: {CHECKLIST_APPLIES[current.key]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="text-xs shrink-0"
|
||||
>
|
||||
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{preview ? (
|
||||
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto">
|
||||
<Markdown content={current.draft} />
|
||||
</div>
|
||||
) : (
|
||||
<Textarea
|
||||
value={current.draft}
|
||||
onChange={(e) => updateDraft(e.target.value)}
|
||||
className="min-h-[400px] font-mono text-sm leading-relaxed"
|
||||
dir="rtl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" disabled={!current.dirty || update.isPending} onClick={handleSave}>
|
||||
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
|
||||
שמור
|
||||
</Button>
|
||||
{current.isOverride && (
|
||||
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
|
||||
<RotateCcw className="w-3 h-3 ml-1" />
|
||||
איפוס לברירת מחדל
|
||||
</Button>
|
||||
<Card className="border-rule shadow-sm overflow-hidden p-0 gap-0">
|
||||
<div className="flex items-center gap-2.5 px-[18px] py-3.5 border-b border-rule-soft">
|
||||
<h2 className="text-[0.95rem] font-semibold text-navy m-0">
|
||||
צ׳קליסט תוכן — {current.label}
|
||||
</h2>
|
||||
<span
|
||||
className={`ms-auto rounded-full text-xs font-semibold px-2.5 py-0.5 ${
|
||||
current.isOverride
|
||||
? "bg-gold-wash text-gold-deep border border-rule"
|
||||
: "bg-info-bg text-info"
|
||||
}`}
|
||||
>
|
||||
{current.isOverride ? "מותאם" : "ידני"}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="text-xs shrink-0 h-7"
|
||||
>
|
||||
{preview ? (
|
||||
<EyeOff className="w-3.5 h-3.5 ms-1" />
|
||||
) : (
|
||||
<Eye className="w-3.5 h-3.5 ms-1" />
|
||||
)}
|
||||
<Badge
|
||||
variant={current.isOverride ? "default" : "secondary"}
|
||||
className="text-[10px] mr-auto"
|
||||
>
|
||||
{current.isOverride ? "מותאם" : "ברירת מחדל"}
|
||||
</Badge>
|
||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{preview ? (
|
||||
<div className="p-[18px] bg-parchment max-h-[500px] overflow-y-auto border-b border-rule-soft">
|
||||
<Markdown content={current.draft} />
|
||||
</div>
|
||||
</CardContent>
|
||||
) : (
|
||||
<Textarea
|
||||
value={current.draft}
|
||||
onChange={(e) => updateDraft(e.target.value)}
|
||||
className="min-h-[340px] rounded-none border-0 border-b border-rule-soft bg-parchment font-mono text-[0.84rem] leading-[1.95] text-ink-soft focus-visible:ring-0 resize-y"
|
||||
dir="rtl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2.5 px-[18px] py-3.5">
|
||||
<Button
|
||||
disabled={!current.dirty || update.isPending}
|
||||
onClick={handleSave}
|
||||
className="bg-gold text-white hover:bg-gold-deep"
|
||||
>
|
||||
{update.isPending ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin ms-1" />
|
||||
) : (
|
||||
<Save className="w-3.5 h-3.5 ms-1" />
|
||||
)}
|
||||
שמור
|
||||
</Button>
|
||||
{current.isOverride && (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={reset.isPending}
|
||||
onClick={handleReset}
|
||||
className="border-rule text-navy"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 ms-1" />
|
||||
אפס לברירת-מחדל
|
||||
</Button>
|
||||
)}
|
||||
<span className="ms-auto text-[0.78rem] text-ink-muted tabular-nums">
|
||||
{itemCount} פריטים
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
useDeleteMissingPrecedent,
|
||||
CITED_BY_PARTY_LABELS,
|
||||
STATUS_LABELS,
|
||||
type CitedByParty,
|
||||
type MissingPrecedent,
|
||||
type MissingPrecedentStatus,
|
||||
} from "@/lib/api/missing-precedents";
|
||||
@@ -29,20 +30,39 @@ function formatDate(iso: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Status chip — mockup 09 tones (open=warn, uploaded=info, closed=success,
|
||||
* irrelevant=muted). Pill-shaped, whitespace-nowrap. */
|
||||
function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
|
||||
const variants: Record<MissingPrecedentStatus, string> = {
|
||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-rule line-through",
|
||||
open: "bg-warn-bg text-warn border-transparent",
|
||||
uploaded: "bg-info-bg text-info border-transparent",
|
||||
closed: "bg-success-bg text-success border-transparent",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-transparent",
|
||||
};
|
||||
return (
|
||||
<Badge variant="outline" className={variants[status]}>
|
||||
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[status]}`}>
|
||||
{STATUS_LABELS[status]}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/** Citing-party chip — colored by side (mockup 09 source chips). */
|
||||
function SourceChip({ party }: { party: CitedByParty | null }) {
|
||||
if (!party) return <span className="text-ink-muted text-sm">—</span>;
|
||||
const variants: Record<CitedByParty, string> = {
|
||||
appellant: "bg-info-bg text-info border-transparent",
|
||||
respondent: "bg-gold-wash text-gold-deep border-rule",
|
||||
committee: "bg-success-bg text-success border-transparent",
|
||||
permit_applicant: "bg-info-bg text-info border-transparent",
|
||||
unknown: "bg-rule-soft text-ink-muted border-transparent",
|
||||
};
|
||||
return (
|
||||
<Badge variant="outline" className={`rounded-full whitespace-nowrap ${variants[party]}`}>
|
||||
{CITED_BY_PARTY_LABELS[party]}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
function TableSkeleton({ cols }: { cols: number }) {
|
||||
return (
|
||||
<>
|
||||
@@ -100,14 +120,14 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
<>
|
||||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader className="bg-rule-soft/60">
|
||||
<TableRow className="border-rule">
|
||||
<TableHead className="text-navy text-right">פסיקה</TableHead>
|
||||
<TableHead className="text-navy text-right">נושא</TableHead>
|
||||
<TableHead className="text-navy text-right">תיק</TableHead>
|
||||
<TableHead className="text-navy text-right">צד מצטט</TableHead>
|
||||
<TableHead className="text-navy text-right">סטטוס</TableHead>
|
||||
<TableHead className="text-navy text-right">נוצר</TableHead>
|
||||
<TableHeader className="bg-parchment">
|
||||
<TableRow className="border-rule hover:bg-transparent">
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">פסיקה</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">נושא</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">תיק</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">צוטט ע״י</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">סטטוס</TableHead>
|
||||
<TableHead className="text-ink-muted text-right font-medium text-xs">נוצר</TableHead>
|
||||
<TableHead className="text-navy" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -128,7 +148,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
onClick={() => setOpenId(mp.id)}
|
||||
>
|
||||
<TableCell className="max-w-[440px]">
|
||||
<div className="text-sm text-navy font-medium truncate">
|
||||
<div className="text-sm text-navy font-semibold truncate">
|
||||
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||||
@@ -153,11 +173,9 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-ink">
|
||||
{mp.cited_by_party
|
||||
? CITED_BY_PARTY_LABELS[mp.cited_by_party]
|
||||
: "—"}
|
||||
<SourceChip party={mp.cited_by_party} />
|
||||
{mp.cited_by_party_name ? (
|
||||
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px]">
|
||||
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px] mt-1">
|
||||
{mp.cited_by_party_name}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -165,7 +183,7 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
<TableCell>
|
||||
<StatusBadge status={mp.status} />
|
||||
{mp.linked_case_law_number ? (
|
||||
<div className="text-[0.7rem] text-emerald-700 mt-1">
|
||||
<div className="text-[0.7rem] text-success mt-1">
|
||||
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -174,22 +192,39 @@ export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props
|
||||
{formatDate(mp.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-end">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
title={mp.status === "open" ? "העלאה" : "פרטים"}
|
||||
>
|
||||
{mp.status === "open" ? (
|
||||
<Upload className="w-4 h-4" />
|
||||
) : (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{mp.status === "open" ? (
|
||||
/* gold "העלה והשלם" CTA (mockup 09 `.btn`) */
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
className="h-7 bg-gold text-white hover:bg-gold-deep border-transparent text-[0.78rem] font-semibold"
|
||||
>
|
||||
<Upload className="w-3.5 h-3.5 me-1" />
|
||||
העלה והשלם
|
||||
</Button>
|
||||
) : (
|
||||
/* passive "done" label for non-open rows */
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-rule-soft text-ink-muted text-[0.78rem] font-medium px-2.5 py-1">
|
||||
{mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]} ✓
|
||||
</span>
|
||||
)}
|
||||
{mp.status !== "open" ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
title="פרטים"
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -24,32 +24,56 @@ function formatDate(iso: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Score chip — boxed, gold-deep, tabular (mockup 07 `.score`). Sits at the
|
||||
* end of the meta row via `ms-auto`. White fill on gold-wash halacha cards. */
|
||||
function ScoreChip({ score, onWash }: { score: number; onWash?: boolean }) {
|
||||
return (
|
||||
<span
|
||||
className={`ms-auto rounded-md border border-rule px-2.5 py-0.5 text-xs font-semibold text-gold-deep tabular-nums ${
|
||||
onWash ? "bg-white" : "bg-surface"
|
||||
}`}
|
||||
>
|
||||
דירוג {score.toFixed(2)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gold/40 bg-gold-wash/40 p-4 space-y-2">
|
||||
<div className="rounded-lg border border-rule bg-gold-wash p-4 shadow-sm space-y-2.5">
|
||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
<Badge className="bg-gold text-navy border-0">הלכה</Badge>
|
||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
||||
{hit.court && <span>· {hit.court}</span>}
|
||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
||||
{hit.precedent_level && <span>· {hit.precedent_level}</span>}
|
||||
{/* PRE-3/PRE-5 (INV-IA5): the derived authority (binding/persuasive)
|
||||
rides on the wire but was dropped here — render it as in the review
|
||||
tab so search shows the same provenance everywhere. */}
|
||||
<Badge className="rounded bg-gold text-white border-0 text-[0.68rem] font-bold tracking-wide">
|
||||
הלכה
|
||||
</Badge>
|
||||
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||
{(hit.court || hit.decision_date || hit.precedent_level) && (
|
||||
<span className="text-ink-muted">
|
||||
{hit.court ? `· ${hit.court}` : ""}
|
||||
{hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
|
||||
{hit.precedent_level ? ` · ${hit.precedent_level}` : ""}
|
||||
</span>
|
||||
)}
|
||||
{/* PRE-3/PRE-5 (INV-IA5): derived authority (binding/persuasive)
|
||||
rides on the wire — render the pill as in the review tab. */}
|
||||
<AuthorityBadge authority={hit.authority} />
|
||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
||||
<ScoreChip score={hit.score} onWash />
|
||||
</div>
|
||||
<p className="text-navy font-medium text-[0.95rem]" dir="rtl">
|
||||
<p className="text-ink font-medium text-[0.95rem] leading-7" dir="rtl">
|
||||
{hit.rule_statement}
|
||||
</p>
|
||||
<blockquote className="text-ink-soft text-sm border-s-2 border-gold ps-3" dir="rtl">
|
||||
<blockquote
|
||||
className="rounded-e border-s-[3px] border-gold bg-white/50 px-3 py-2 text-sm text-ink-soft leading-7"
|
||||
dir="rtl"
|
||||
>
|
||||
“{hit.supporting_quote}”
|
||||
{hit.page_reference && <span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>}
|
||||
{hit.page_reference && (
|
||||
<span className="text-ink-muted text-[0.72rem] ms-2">({hit.page_reference})</span>
|
||||
)}
|
||||
</blockquote>
|
||||
{hit.subject_tags?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{hit.subject_tags.map((t) => (
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-surface">
|
||||
<Badge key={t} variant="outline" className="text-[0.65rem] bg-white">
|
||||
{t}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -61,16 +85,24 @@ function HalachaCard({ hit }: { hit: Extract<SearchHit, { type: "halacha" }> })
|
||||
|
||||
function PassageCard({ hit }: { hit: Extract<SearchHit, { type: "passage" }> }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-rule bg-surface p-4 space-y-2">
|
||||
<div className="rounded-lg border border-rule bg-surface p-4 shadow-sm space-y-2.5">
|
||||
<div className="flex items-center gap-2 text-[0.78rem] text-ink-muted flex-wrap">
|
||||
<Badge variant="outline" className="bg-info-bg text-info border-transparent">קטע</Badge>
|
||||
<span className="font-mono" dir="ltr">{hit.case_number}</span>
|
||||
{hit.court && <span>· {hit.court}</span>}
|
||||
{hit.decision_date && <span>· {formatDate(hit.decision_date)}</span>}
|
||||
<span className="text-[0.7rem]">· {hit.section_type}</span>
|
||||
<span className="ms-auto tabular-nums">דירוג {hit.score.toFixed(2)}</span>
|
||||
<Badge variant="outline" className="rounded bg-info-bg text-info border-transparent text-[0.68rem] font-bold tracking-wide">
|
||||
קטע
|
||||
</Badge>
|
||||
<span className="text-navy font-semibold" dir="ltr">{hit.case_number}</span>
|
||||
{(hit.court || hit.decision_date) && (
|
||||
<span className="text-ink-muted">
|
||||
{hit.court ? `· ${hit.court}` : ""}
|
||||
{hit.decision_date ? ` · ${formatDate(hit.decision_date)}` : ""}
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded bg-rule-soft text-ink-muted text-[0.68rem] px-2 py-0.5 font-medium">
|
||||
{hit.section_type}
|
||||
</span>
|
||||
<ScoreChip score={hit.score} />
|
||||
</div>
|
||||
<p className="text-ink text-sm leading-relaxed" dir="rtl">
|
||||
<p className="text-ink-soft text-sm leading-7" dir="rtl">
|
||||
{hit.content.slice(0, 600)}
|
||||
{hit.content.length > 600 && <span>…</span>}
|
||||
</p>
|
||||
@@ -98,51 +130,61 @@ export function LibrarySearchPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="flex items-end gap-3 flex-wrap">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">שאילתת חיפוש</label>
|
||||
<div className="space-y-6">
|
||||
{/* search panel — boxed surface with query row + two-up filter row +
|
||||
controls strip (checkbox start, gold CTA end) per mockup 07. */}
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className="rounded-lg border border-rule bg-surface shadow-sm p-5 space-y-3.5"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">
|
||||
שאילתת חיפוש
|
||||
</label>
|
||||
<Input value={draft} onChange={(e) => setDraft(e.target.value)}
|
||||
placeholder="השבחה אובייקטיבית" dir="rtl" />
|
||||
</div>
|
||||
<div className="min-w-[180px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">תחום</label>
|
||||
<Select value={practiceArea || "_all"}
|
||||
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
|
||||
<div>
|
||||
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">תחום</label>
|
||||
<Select value={practiceArea || "_all"}
|
||||
onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
||||
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<SelectItem key={a.value} value={a.value}>{a.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[0.78rem] text-ink-muted font-medium mb-1.5">רמת תקדים</label>
|
||||
<Select value={precedentLevel || "_all"}
|
||||
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
|
||||
<SelectTrigger className="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-[170px]">
|
||||
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label>
|
||||
<Select value={precedentLevel || "_all"}
|
||||
onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">הכל</SelectItem>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-4 flex-wrap pt-1">
|
||||
<label className="flex items-center gap-2 cursor-pointer text-[0.85rem] text-ink-soft">
|
||||
<input type="checkbox" className="w-[15px] h-[15px] accent-gold" checked={includeHalachot}
|
||||
onChange={(e) => setIncludeHalachot(e.target.checked)} />
|
||||
כלול הלכות
|
||||
</label>
|
||||
<Button type="submit" className="ms-auto bg-gold text-white hover:bg-gold-deep border-transparent">
|
||||
<Search className="w-4 h-4 me-1" />
|
||||
חפש
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="submit" className="bg-navy text-parchment hover:bg-navy-soft">
|
||||
<Search className="w-4 h-4 me-1" />
|
||||
חפש
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer text-sm text-ink-muted">
|
||||
<input type="checkbox" checked={includeHalachot}
|
||||
onChange={(e) => setIncludeHalachot(e.target.checked)} />
|
||||
כלול הלכות (rule-level matches)
|
||||
</label>
|
||||
|
||||
{!query.trim() ? (
|
||||
<div className="text-center text-ink-muted py-12">
|
||||
הקלד שאילתא כדי לחפש בקורפוס. החיפוש סמנטי — לא טקסטואלי.
|
||||
@@ -160,11 +202,13 @@ export function LibrarySearchPanel() {
|
||||
לא נמצאו תוצאות. נסה ניסוח אחר או הסר פילטרים.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<p className="text-[0.78rem] text-ink-muted flex items-center gap-2">
|
||||
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.7rem] font-semibold px-2.5 py-0.5">
|
||||
<div className="space-y-3.5">
|
||||
<p className="text-[0.82rem] text-ink-muted flex items-center gap-2">
|
||||
<span>תוצאות</span>
|
||||
<span className="inline-flex items-center rounded-full bg-success-bg text-success text-[0.72rem] font-semibold px-2.5 py-0.5">
|
||||
הלכות מאושרות בלבד
|
||||
</span>
|
||||
<span aria-hidden>·</span>
|
||||
<span className="tabular-nums">{data.count} תוצאות</span>
|
||||
</p>
|
||||
{data.items.map((hit, i) =>
|
||||
|
||||
@@ -128,60 +128,6 @@ function LinkDialog({ caseId, currentRelated, open, onOpenChange }: DialogProps)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Related Case Card ────────────────────────────────────────────────
|
||||
|
||||
function RelatedCaseCard({ caseId, related }: { caseId: string; related: RelatedCase }) {
|
||||
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
|
||||
|
||||
async function handleUnlink() {
|
||||
try {
|
||||
await unlinkCase(related.id);
|
||||
toast.success("הקישור הוסר");
|
||||
} catch {
|
||||
toast.error("שגיאה בהסרת הקישור");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg border border-rule bg-surface">
|
||||
<a
|
||||
href={`/precedents/${related.id}`}
|
||||
className="min-w-0 flex-1 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<div className="text-sm font-medium text-navy truncate">
|
||||
{related.case_name || related.case_number}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
{related.precedent_level && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[0.62rem] ${LEVEL_COLORS[related.precedent_level] ?? ""}`}
|
||||
>
|
||||
{LEVEL_LABELS[related.precedent_level] ?? related.precedent_level}
|
||||
</Badge>
|
||||
)}
|
||||
{related.court && (
|
||||
<span className="text-[0.7rem] text-ink-muted truncate">{related.court}</span>
|
||||
)}
|
||||
{related.date && (
|
||||
<span className="text-[0.7rem] text-ink-muted tabular-nums" dir="ltr">
|
||||
{related.date.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<button
|
||||
onClick={handleUnlink}
|
||||
disabled={isPending}
|
||||
className="p-1 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
|
||||
title="הסר קישור"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Public section component ─────────────────────────────────────────
|
||||
|
||||
type SectionProps = {
|
||||
@@ -189,28 +135,68 @@ type SectionProps = {
|
||||
related: RelatedCase[];
|
||||
};
|
||||
|
||||
/* Rail-styled citations card (mockup 08 side rail). Renders linked related
|
||||
* decisions as a navy-headed card with arrow-prefixed rows; keeps the full
|
||||
* link/unlink logic. Used in the precedent-detail side rail. */
|
||||
export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-navy text-sm font-semibold">
|
||||
החלטות קשורות{related.length > 0 ? ` (${related.length})` : ""}
|
||||
<div className="rounded-lg border border-rule bg-surface shadow-sm px-4 py-3.5 space-y-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-navy text-[0.92rem] font-semibold m-0">
|
||||
ציטוטים מקושרים{related.length > 0 ? ` (${related.length})` : ""}
|
||||
</h3>
|
||||
<Button variant="outline" size="sm" onClick={() => setDialogOpen(true)}>
|
||||
<Link2 className="w-3.5 h-3.5 me-1" /> קשר החלטה
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[0.72rem] border-rule"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
<Link2 className="w-3 h-3 me-1" /> קשר
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{related.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין החלטות קשורות עדיין</p>
|
||||
<p className="text-ink-muted text-[0.82rem] m-0">אין החלטות קשורות עדיין</p>
|
||||
) : (
|
||||
<div className="space-y-1.5">
|
||||
<ul className="list-none p-0 m-0">
|
||||
{related.map((r) => (
|
||||
<RelatedCaseCard key={r.id} caseId={caseId} related={r} />
|
||||
<li
|
||||
key={r.id}
|
||||
className="flex items-start gap-2 py-2 border-b border-rule-soft last:border-b-0"
|
||||
>
|
||||
<span className="text-gold font-bold leading-6 shrink-0" aria-hidden>←</span>
|
||||
<a
|
||||
href={`/precedents/${r.id}`}
|
||||
className="min-w-0 flex-1 group hover:opacity-90 transition-opacity"
|
||||
>
|
||||
<div className="text-[0.82rem] text-ink-soft leading-5 group-hover:text-navy">
|
||||
{r.case_name || r.case_number}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
{r.precedent_level && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-[0.6rem] ${LEVEL_COLORS[r.precedent_level] ?? ""}`}
|
||||
>
|
||||
{LEVEL_LABELS[r.precedent_level] ?? r.precedent_level}
|
||||
</Badge>
|
||||
)}
|
||||
{r.court && (
|
||||
<span className="text-[0.68rem] text-ink-muted truncate">{r.court}</span>
|
||||
)}
|
||||
{r.date && (
|
||||
<span className="text-[0.68rem] text-ink-muted tabular-nums" dir="ltr">
|
||||
{r.date.slice(0, 10)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
<UnlinkButton caseId={caseId} relatedId={r.id} />
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<LinkDialog
|
||||
@@ -222,3 +208,24 @@ export function RelatedCasesSection({ caseId, related }: SectionProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnlinkButton({ caseId, relatedId }: { caseId: string; relatedId: string }) {
|
||||
const { mutateAsync: unlinkCase, isPending } = useUnlinkRelatedCase(caseId);
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await unlinkCase(relatedId);
|
||||
toast.success("הקישור הוסר");
|
||||
} catch {
|
||||
toast.error("שגיאה בהסרת הקישור");
|
||||
}
|
||||
}}
|
||||
disabled={isPending}
|
||||
className="p-0.5 rounded hover:bg-danger-bg hover:text-danger transition-colors text-ink-muted disabled:opacity-40 shrink-0"
|
||||
title="הסר קישור"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { SubjectDonut } from "@/components/training/subject-donut";
|
||||
import { useStyleReport } from "@/lib/api/training";
|
||||
import { useStyleReport, useCuratorStats } from "@/lib/api/training";
|
||||
|
||||
// Mockup 12 anatomy palette — info · gold · gold-deep · success, cycling.
|
||||
const ANATOMY_COLORS = ["#4e6a8c", "#a97d3a", "#8b6428", "#4a7c59"];
|
||||
|
||||
function KPICard({
|
||||
label,
|
||||
@@ -16,15 +19,13 @@ function KPICard({
|
||||
}) {
|
||||
return (
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-5 py-4 flex flex-col gap-0.5">
|
||||
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
|
||||
{label}
|
||||
</span>
|
||||
<span className="font-display text-[2rem] font-black leading-none text-navy">
|
||||
<CardContent className="px-[18px] py-4 flex flex-col">
|
||||
<span className="font-display text-[1.85rem] font-bold leading-[1.1] text-navy tabular-nums">
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-[0.81rem] text-ink-soft mt-1">{label}</span>
|
||||
{caption && (
|
||||
<span className="text-[0.78rem] text-ink-muted mt-1">{caption}</span>
|
||||
<span className="text-[0.72rem] text-ink-muted mt-0.5">{caption}</span>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -33,6 +34,7 @@ function KPICard({
|
||||
|
||||
export function StyleReportPanel() {
|
||||
const { data, isPending, error } = useStyleReport();
|
||||
const curator = useCuratorStats();
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -63,14 +65,13 @@ export function StyleReportPanel() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Headline */}
|
||||
<Card className="bg-gold-wash border-gold/40 shadow-sm">
|
||||
<CardContent className="px-6 py-4">
|
||||
<p className="font-display text-gold-deep text-lg font-semibold leading-snug">
|
||||
★ {c.headline}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Headline banner — gold-wash, ★ aligned to start (mockup 12) */}
|
||||
<div className="flex items-start gap-3 rounded-lg border border-gold bg-gold-wash px-5 py-4 shadow-sm">
|
||||
<span className="text-gold-deep text-xl leading-tight shrink-0">★</span>
|
||||
<p className="text-ink-soft text-[0.95rem] leading-relaxed m-0">
|
||||
{c.headline}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||
@@ -121,23 +122,22 @@ export function StyleReportPanel() {
|
||||
{data.anatomy.sections.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין נתונים על מבנה</p>
|
||||
) : (
|
||||
<ul className="space-y-2.5">
|
||||
{data.anatomy.sections.map((s) => {
|
||||
<ul className="space-y-3.5">
|
||||
{data.anatomy.sections.map((s, i) => {
|
||||
const pct = Math.round(s.pct * 100);
|
||||
const fill = ANATOMY_COLORS[i % ANATOMY_COLORS.length];
|
||||
return (
|
||||
<li key={s.type} className="space-y-1">
|
||||
<div className="flex items-center justify-between text-[0.78rem]">
|
||||
<span className="text-ink-soft font-medium">
|
||||
{s.label}
|
||||
</span>
|
||||
<span className="text-ink-muted tabular-nums">
|
||||
<li key={s.type} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-[0.81rem]">
|
||||
<span className="text-ink-soft">{s.label}</span>
|
||||
<span className="text-navy font-semibold tabular-nums">
|
||||
{pct}% · {s.avg_chars.toLocaleString()} תווים
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded bg-rule-soft overflow-hidden">
|
||||
<div className="h-2.5 rounded-full bg-rule-soft overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-l from-gold to-gold-deep"
|
||||
style={{ width: `${pct}%` }}
|
||||
className="h-full rounded-full"
|
||||
style={{ width: `${pct}%`, backgroundColor: fill }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
@@ -149,50 +149,92 @@ export function StyleReportPanel() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Signature phrases */}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
|
||||
{data.signature_phrases.headline && (
|
||||
<p className="text-[0.78rem] text-gold-deep mb-4">
|
||||
{data.signature_phrases.headline}
|
||||
</p>
|
||||
)}
|
||||
{data.signature_phrases.items.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
|
||||
) : (
|
||||
<ol className="space-y-2">
|
||||
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
|
||||
<li
|
||||
key={`${p.type}-${i}`}
|
||||
className="flex items-start gap-3 rounded border border-rule bg-parchment/40 px-3 py-2"
|
||||
>
|
||||
<span className="text-[0.7rem] text-ink-muted tabular-nums shrink-0 mt-0.5">
|
||||
#{i + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-ink leading-relaxed text-sm">{p.text}</p>
|
||||
{p.context && (
|
||||
<p className="text-[0.7rem] text-ink-muted mt-0.5">
|
||||
{p.context}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="
|
||||
shrink-0 text-[0.72rem] rounded-full
|
||||
bg-gold-wash text-gold-deep border border-gold/40
|
||||
px-2 py-0.5 tabular-nums
|
||||
"
|
||||
{/* Signature phrases + curator stat — two columns (mockup 12) */}
|
||||
<div className="grid gap-6 lg:grid-cols-2 items-start">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5">
|
||||
<h3 className="text-navy text-lg mb-1">ביטויי חתימה</h3>
|
||||
{data.signature_phrases.headline && (
|
||||
<p className="text-[0.78rem] text-gold-deep mb-3">
|
||||
{data.signature_phrases.headline}
|
||||
</p>
|
||||
)}
|
||||
{data.signature_phrases.items.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין ביטויים שחולצו עדיין</p>
|
||||
) : (
|
||||
<ol>
|
||||
{data.signature_phrases.items.slice(0, 12).map((p, i) => (
|
||||
<li
|
||||
key={`${p.type}-${i}`}
|
||||
className="flex items-baseline gap-2.5 py-2.5 border-b border-rule-soft last:border-b-0"
|
||||
>
|
||||
×{p.frequency}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<span className="w-5 shrink-0 text-gold-deep font-bold tabular-nums text-sm">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-ink-soft leading-relaxed text-[0.85rem] m-0">
|
||||
{p.text}
|
||||
</p>
|
||||
{p.context && (
|
||||
<p className="text-[0.7rem] text-ink-muted mt-0.5 m-0">
|
||||
{p.context}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="shrink-0 text-[0.72rem] font-semibold rounded-full bg-gold-wash text-gold-deep border border-rule px-2.5 py-0.5 tabular-nums whitespace-nowrap">
|
||||
×{p.frequency}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Curator — surfaced style findings (INV-LRN1 writer gate) */}
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-6 py-5 flex flex-col gap-4">
|
||||
<h3 className="text-navy text-lg m-0">אוצֵר — ממצאי-סגנון</h3>
|
||||
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
|
||||
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
|
||||
{curator.data ? curator.data.findings_approved : "—"}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<b className="text-navy text-sm font-semibold block">
|
||||
ממצאים מאושרים (זורמים לכותב)
|
||||
</b>
|
||||
<span className="text-ink-muted text-[0.78rem]">
|
||||
אושרו ע״י היו״ר · review_status=approved
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3.5">
|
||||
<div className="flex-1 rounded-lg border border-rule bg-warn-bg px-3.5 py-3">
|
||||
<div className="text-warn font-bold text-[1.35rem] tabular-nums leading-none">
|
||||
{curator.data
|
||||
? Math.max(
|
||||
0,
|
||||
curator.data.total_findings -
|
||||
curator.data.findings_approved,
|
||||
)
|
||||
: "—"}
|
||||
</div>
|
||||
<div className="text-[0.78rem] text-ink-soft mt-1">לא-מאושרים</div>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg border border-rule bg-rule-soft px-3.5 py-3">
|
||||
<div className="text-ink-muted font-bold text-[1.35rem] tabular-nums leading-none">
|
||||
{curator.data ? curator.data.total_findings : "—"}
|
||||
</div>
|
||||
<div className="text-[0.78rem] text-ink-soft mt-1">סך ממצאים</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[0.72rem] text-ink-muted leading-relaxed m-0">
|
||||
רק ממצא מאושר זורם לכותב (INV-LRN1). ממתינים ונדחים אינם משפיעים על
|
||||
הטיוטות.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user