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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user