Files
legal-ai/web-ui/src/components/wizard/parties-field.tsx
Chaim cbe9d60901 Phase 6: polish — error boundaries, a11y, smoke test doc
Close out the read-only surface before cutover with three families of
small fixes that the previous phases left unfinished:

- Error boundaries: add src/app/error.tsx (route-segment), global-error.tsx
  (root crash fallback with its own minimal html/body — no Providers
  dependency since those may be the thing that crashed), and not-found.tsx
  for a Hebrew 404 instead of the default Next page.

- Accessibility: wire usePathname() into AppShell so the current nav item
  gets aria-current="page" and a gold underline. Add aria-label + aria-hidden
  on the icon-only buttons that Phase 5 left text-less (corpus trash,
  parties-field Plus). Nav gets an aria-label of its own.

- Metadata template: title on each route now reads "X · עוזר משפטי" via
  the layout.tsx title.template. Description localized to Jerusalem.

- README: full E2E smoke test checklist covering all 9 screens, plus a
  backend contract table so future phases know which hook wraps which
  endpoint. Documents the known Gitea→Coolify webhook issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:43:59 +00:00

96 lines
2.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { X, Plus } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
/*
* Minimal tag-style editor for a list of party names (appellants / respondents).
* Backed by a controlled string[] — submits as the same shape the FastAPI
* CaseCreateRequest expects. Enter adds the current draft; X removes a chip.
*/
export function PartiesField({
label,
value,
onChange,
error,
}: {
label: string;
value: string[];
onChange: (next: string[]) => void;
error?: string;
}) {
const [draft, setDraft] = useState("");
const add = () => {
const trimmed = draft.trim();
if (!trimmed) return;
if (value.includes(trimmed)) {
setDraft("");
return;
}
onChange([...value, trimmed]);
setDraft("");
};
const remove = (name: string) => {
onChange(value.filter((v) => v !== name));
};
return (
<div>
<label className="block text-sm font-medium text-navy mb-1.5">{label}</label>
{value.length > 0 && (
<ul className="flex flex-wrap gap-2 mb-2">
{value.map((name) => (
<li
key={name}
className="
inline-flex items-center gap-1.5 rounded-full
bg-gold-wash text-gold-deep border border-gold/40
px-3 py-1 text-sm
"
>
<span>{name}</span>
<button
type="button"
onClick={() => remove(name)}
className="hover:text-danger transition-colors"
aria-label={`הסר ${name}`}
>
<X className="w-3.5 h-3.5" />
</button>
</li>
))}
</ul>
)}
<div className="flex gap-2">
<Input
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
add();
}
}}
placeholder="שם מלא של הצד"
dir="rtl"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={add}
aria-label={`הוסף ${label}`}
>
<Plus className="w-4 h-4" aria-hidden="true" />
</Button>
</div>
{error && <p className="text-[0.72rem] text-danger mt-1">{error}</p>}
</div>
);
}