feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
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:
2026-06-11 23:00:25 +00:00
parent c53ef9a7c4
commit f3b075d282
32 changed files with 2925 additions and 1799 deletions

View File

@@ -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="בקשה להארכת מועד להגשת ערר"
>
בל&quot;מ
</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="בקשה להארכת מועד להגשת ערר"
>
בל&quot;מ
</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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}