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>
This commit is contained in:
@@ -848,13 +848,13 @@
|
||||
"description": "Port the remaining 5 views. Use TanStack Table for training corpus and diagnostics lists. Port any charts/visualizations from current index.html. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 5 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Feature parity with old legal-ai/web/static/index.html across all 10 views.",
|
||||
"status": "in-progress",
|
||||
"status": "done",
|
||||
"dependencies": [
|
||||
"86"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T17:28:06.562Z"
|
||||
"updatedAt": "2026-04-11T17:33:42.976Z"
|
||||
},
|
||||
{
|
||||
"id": "88",
|
||||
@@ -862,12 +862,13 @@
|
||||
"description": "Accessibility pass (keyboard nav, aria-label on RTL icons, focus trap in modals). Error boundaries + toast notifications for failed mutations. Loading states for every query. Cross-browser smoke test (Chrome, Firefox, Safari) + mobile device test. Document E2E smoke test script in web-ui/README.md. Plan: ~/.claude/plans/joyful-marinating-sutton.md.",
|
||||
"details": "See full plan at ~/.claude/plans/joyful-marinating-sutton.md for architecture, critical files, risks, and open questions. This task is phase 6 of 7 in the legal-ai UI rewrite from vanilla HTML to Next.js 15 + shadcn/ui.",
|
||||
"testStrategy": "Lighthouse a11y score > 90, all loading states visible, errors show toasts, README has documented smoke test steps.",
|
||||
"status": "pending",
|
||||
"status": "in-progress",
|
||||
"dependencies": [
|
||||
"87"
|
||||
],
|
||||
"priority": "medium",
|
||||
"subtasks": []
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-04-11T17:40:09.247Z"
|
||||
},
|
||||
{
|
||||
"id": "89",
|
||||
@@ -899,9 +900,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-04-11T17:28:06.563Z",
|
||||
"lastModified": "2026-04-11T17:40:09.248Z",
|
||||
"taskCount": 59,
|
||||
"completedCount": 54,
|
||||
"completedCount": 55,
|
||||
"tags": [
|
||||
"master"
|
||||
]
|
||||
|
||||
181
web-ui/README.md
181
web-ui/README.md
@@ -1,36 +1,175 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# עוזר משפטי — Web UI (Next.js rewrite)
|
||||
|
||||
## Getting Started
|
||||
The Next.js 16 rewrite of `legal-ai.nautilus.marcusgroup.org`, currently hosted side-by-side with the legacy vanilla `index.html` at:
|
||||
|
||||
First, run the development server:
|
||||
- **Staging:** https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from `ui-rewrite` branch via Coolify)
|
||||
- **Production FastAPI:** https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
|
||||
|
||||
The rewrite talks to the existing FastAPI via proxy rewrites in `next.config.ts` — no CORS setup, no duplicated backend.
|
||||
|
||||
## Stack
|
||||
|
||||
- Next.js 16.2.3 (App Router, Turbopack, `output: "standalone"`)
|
||||
- React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
|
||||
- TanStack Query v5 + TanStack Table v8
|
||||
- react-hook-form + zod for mutations
|
||||
- react-dropzone for uploads; EventSource for SSE progress
|
||||
- Heebo via `next/font/google`; design tokens in `src/app/globals.css`
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npm run build # full type check + production build
|
||||
npm run lint
|
||||
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
### API connection
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
By default the dev server proxies to production FastAPI (`https://legal-ai.nautilus.marcusgroup.org`). To point at a different backend, set:
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
```bash
|
||||
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Learn More
|
||||
## Project layout
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
```
|
||||
src/
|
||||
├── app/ # Route segments (App Router)
|
||||
│ ├── layout.tsx # Root: Providers + RTL html + fonts
|
||||
│ ├── page.tsx # Home: KPIs + status donut + cases table
|
||||
│ ├── error.tsx # Route-segment error boundary
|
||||
│ ├── global-error.tsx # Root crash fallback
|
||||
│ ├── not-found.tsx # 404
|
||||
│ ├── cases/
|
||||
│ │ ├── new/ # 3-step create wizard
|
||||
│ │ └── [caseNumber]/
|
||||
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
|
||||
│ │ └── compose/ # Research analysis + chair-position editor
|
||||
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
|
||||
│ ├── skills/ # Paperclip skills inventory
|
||||
│ └── diagnostics/ # DB health + failed/stuck docs
|
||||
├── components/
|
||||
│ ├── app-shell.tsx # Header + nav with aria-current
|
||||
│ ├── cases/ # Home + detail screens
|
||||
│ ├── compose/ # Research analysis editor
|
||||
│ ├── documents/ # UploadSheet
|
||||
│ ├── training/ # Style report / corpus / compare panels
|
||||
│ ├── wizard/ # Case create wizard + parties-field
|
||||
│ └── ui/ # shadcn primitives
|
||||
├── lib/
|
||||
│ ├── api/ # Typed hooks per domain (cases, documents, research,
|
||||
│ │ # system, skills, training)
|
||||
│ ├── schemas/ # zod schemas (case create / update)
|
||||
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
|
||||
│ ├── sse.ts # EventSource wrapper
|
||||
│ ├── providers.tsx # QueryClient + Toaster
|
||||
│ └── utils.ts # cn()
|
||||
```
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
## Smoke test (run after every deploy)
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
Use any browser at the staging URL. Every step should be doable **without console errors** and each mutation should produce a visible toast.
|
||||
|
||||
## Deploy on Vercel
|
||||
### 1. Home · `/`
|
||||
- [ ] Header nav shows 5 items; the current page is underlined in gold
|
||||
- [ ] 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
|
||||
- [ ] Cases table lists existing cases; search filters by case number or title
|
||||
- [ ] "פיזור סטטוסים" donut renders with a legend
|
||||
- [ ] "+ תיק חדש" button in the top-left navigates to `/cases/new`
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
### 2. Create case · `/cases/new`
|
||||
- [ ] 3-step wizard: פרטי יסוד → צדדים → השלמות
|
||||
- [ ] Type `1500-25` → appeal_subtype auto-fills to "רישוי ובנייה"
|
||||
- [ ] Type `8500-25` → subtype auto-fills to "היטל השבחה"
|
||||
- [ ] Manually pick a different subtype → auto-fill stops
|
||||
- [ ] Submitting with invalid case number shows a zod field error (no crash)
|
||||
- [ ] Successful create → toast "תיק חדש נוצר" → router pushes to `/cases/{number}`
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
### 3. Case detail · `/cases/[caseNumber]`
|
||||
- [ ] Header shows status badge + gold "ועדת ערר · X" practice-area badge
|
||||
- [ ] Tabs switch cleanly: סקירה / מסמכים / פעולות
|
||||
- [ ] Workflow timeline on the right shows the current phase highlighted in gold
|
||||
- [ ] פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
|
||||
- [ ] "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
|
||||
|
||||
### 4. Compose · `/cases/[caseNumber]/compose`
|
||||
- [ ] If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
|
||||
- [ ] Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
|
||||
- [ ] If 404 (no analysis yet): empty state card renders, no error toast
|
||||
|
||||
### 5. Training · `/training`
|
||||
- [ ] **Report tab:** headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
|
||||
- [ ] **Corpus tab:** table of corpus decisions with a trash icon per row (aria-label present)
|
||||
- [ ] Deleting a decision refreshes both the corpus table and the report KPIs
|
||||
- [ ] **Compare tab:** two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
|
||||
|
||||
### 6. Skills · `/skills`
|
||||
- [ ] Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
|
||||
- [ ] Chars + file counts render; "לא ידוע" doesn't appear for installed skills
|
||||
|
||||
### 7. Diagnostics · `/diagnostics`
|
||||
- [ ] DB status card shows "מחובר" in green
|
||||
- [ ] Table counts populate for cases / documents / chunks / corpus / patterns
|
||||
- [ ] Failed + stuck document lists render (empty states OK)
|
||||
- [ ] Page self-refreshes every 10s — check the network tab for recurring calls
|
||||
|
||||
### 8. Error boundary
|
||||
- [ ] Visit `/cases/NOT-REAL-999-99` → case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen)
|
||||
- [ ] Visit `/anything-broken-xyz` → custom 404 page with "חזרה לבית" button
|
||||
|
||||
### 9. Keyboard + RTL
|
||||
- [ ] Tab through the home page — focus rings are gold, visible
|
||||
- [ ] Wizard progresses via Enter on the "הבא" button
|
||||
- [ ] Screen reader announces nav items with "עמוד נוכחי" on the active one
|
||||
|
||||
## Deploy
|
||||
|
||||
```
|
||||
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
|
||||
```
|
||||
|
||||
> **Known issue:** the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (`mcp__coolify__deploy` with app UUID `l146g36mtlp0k03vrwkyrgkk`) or the Coolify UI until the webhook is fixed.
|
||||
|
||||
## Phase tracking
|
||||
|
||||
See `~/.claude/plans/joyful-marinating-sutton.md` for the 7-phase rewrite plan and `~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json` for the task board.
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|---|---|---|
|
||||
| 1 | Scaffold + Coolify staging | ✅ |
|
||||
| 2 | API client + typed hooks + probe | ✅ |
|
||||
| 3 | Read views (home, case detail, compose) | ✅ |
|
||||
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
|
||||
| 4.5 | Practice-area integration | ✅ |
|
||||
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
|
||||
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
|
||||
| 7 | DNS cutover to production | pending |
|
||||
|
||||
## Backend contract
|
||||
|
||||
The new UI consumes the existing FastAPI at `legal-ai/web/app.py`. Key endpoints currently relied on:
|
||||
|
||||
| Endpoint | Hook | Used by |
|
||||
|---|---|---|
|
||||
| `GET /api/cases?detail=true` | `useCases` | home table, KPIs |
|
||||
| `GET /api/cases/{n}/details` | `useCase` | case detail |
|
||||
| `POST /api/cases/create` | `useCreateCase` | wizard |
|
||||
| `PUT /api/cases/{n}` | `useUpdateCase` | inline edit |
|
||||
| `DELETE /api/cases?case_number=...` | (MCP only so far) | admin cleanup |
|
||||
| `POST /api/cases/{n}/documents/upload-tagged` | `useUploadDocument` | upload sheet |
|
||||
| `GET /api/progress/{task_id}` (SSE) | `useProgress` | upload progress |
|
||||
| `GET /api/cases/{n}/research/analysis` | `useResearchAnalysis` | compose |
|
||||
| `PATCH .../chair-position` | `useSaveChairPosition` | chair editor |
|
||||
| `GET /api/training/style-report` | `useStyleReport` | training/report tab |
|
||||
| `GET /api/training/corpus` | `useCorpus` | training/corpus tab |
|
||||
| `GET /api/training/compare` | `useCompare` | training/compare tab |
|
||||
| `DELETE /api/training/corpus/{id}` | `useDeleteCorpusEntry` | corpus tab |
|
||||
| `GET /api/system/diagnostics` | `useDiagnostics` | diagnostics page |
|
||||
| `GET /api/admin/skills` | `useSkills` | skills page |
|
||||
|
||||
Any new endpoint should get a typed hook in `src/lib/api/` — do not reach into `fetch` from component code.
|
||||
|
||||
57
web-ui/src/app/error.tsx
Normal file
57
web-ui/src/app/error.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
/*
|
||||
* Route-segment error boundary. Next 16 App Router convention: this file
|
||||
* catches render-time errors thrown below the root layout. `reset` clears
|
||||
* the error and re-renders the segment; we keep the user's nav in place
|
||||
* (no AppShell here — the shell re-renders from the layout above us).
|
||||
*/
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error("Route error:", error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<main className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10">
|
||||
<div className="max-w-xl mx-auto text-center space-y-5 py-16">
|
||||
<AlertTriangle className="w-12 h-12 text-danger mx-auto" aria-hidden="true" />
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-navy">משהו השתבש</h1>
|
||||
<p className="text-ink-muted leading-relaxed">
|
||||
נכשלה טעינת המסך. זה עשוי להיות כשל זמני ברשת או באחד מה-endpoints
|
||||
של FastAPI.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p className="text-[0.72rem] text-ink-light tabular-nums">
|
||||
error id: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
{error.message && (
|
||||
<code className="text-[0.78rem] text-ink-soft bg-rule-soft/60 rounded px-3 py-1 inline-block mt-2">
|
||||
{error.message}
|
||||
</code>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Button onClick={reset} className="bg-navy hover:bg-navy-soft text-parchment">
|
||||
נסה שוב
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/">חזרה לבית</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
60
web-ui/src/app/global-error.tsx
Normal file
60
web-ui/src/app/global-error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
/*
|
||||
* Root-level error boundary. Renders only when the root layout itself
|
||||
* crashes, so it must include its own <html> / <body>. No AppShell here —
|
||||
* the Providers that wrap AppShell are also above the crash boundary and
|
||||
* may themselves be the thing that failed.
|
||||
*/
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<html lang="he" dir="rtl">
|
||||
<body
|
||||
style={{
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
background: "#f5f1e8",
|
||||
color: "#1a1a2e",
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "2rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 520, textAlign: "center" }}>
|
||||
<h1 style={{ color: "#0f172a", fontSize: "2rem", marginBottom: 8 }}>
|
||||
שגיאה חמורה
|
||||
</h1>
|
||||
<p style={{ color: "#6b7280", lineHeight: 1.6, marginBottom: 16 }}>
|
||||
האפליקציה נכשלה לטעון. נסה לרענן את הדף.
|
||||
</p>
|
||||
{error.digest && (
|
||||
<p style={{ fontSize: 12, color: "#9ca3af", marginBottom: 16 }}>
|
||||
error id: {error.digest}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={reset}
|
||||
style={{
|
||||
background: "#0f172a",
|
||||
color: "#fbf8f0",
|
||||
border: 0,
|
||||
padding: "0.6rem 1.25rem",
|
||||
borderRadius: 6,
|
||||
cursor: "pointer",
|
||||
fontSize: "0.95rem",
|
||||
}}
|
||||
>
|
||||
נסה שוב
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -14,8 +14,11 @@ const heebo = Heebo({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "עוזר משפטי — ניהול תיקים",
|
||||
description: "מערכת סיוע בניסוח החלטות לוועדת ערר לתכנון ובנייה",
|
||||
title: {
|
||||
default: "עוזר משפטי — ניהול תיקים",
|
||||
template: "%s · עוזר משפטי",
|
||||
},
|
||||
description: "מערכת סיוע בניסוח החלטות לוועדת ערר לתכנון ובנייה, ירושלים",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
24
web-ui/src/app/not-found.tsx
Normal file
24
web-ui/src/app/not-found.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/app-shell";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<AppShell>
|
||||
<section className="max-w-xl mx-auto text-center py-16 space-y-5">
|
||||
<div className="font-display text-gold text-6xl leading-none" aria-hidden="true">
|
||||
404
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-navy">הדף לא נמצא</h1>
|
||||
<p className="text-ink-muted leading-relaxed">
|
||||
הכתובת שביקשת אינה קיימת או שהוזזה לדף אחר.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild className="bg-navy hover:bg-navy-soft text-parchment">
|
||||
<Link href="/">חזרה לבית</Link>
|
||||
</Button>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Ezer Mishpati navigation shell.
|
||||
@@ -9,8 +12,9 @@ import Link from "next/link";
|
||||
* - Parchment/cream body background (set on <body> via globals.css)
|
||||
* - Hebrew RTL throughout (set on <html> in layout.tsx)
|
||||
*
|
||||
* Structure mirrors the current vanilla index.html header so that visual
|
||||
* continuity is preserved while we migrate screen-by-screen.
|
||||
* Nav items pick up an `aria-current="page"` and a gold underline when
|
||||
* the current route matches, so screen readers announce the active
|
||||
* section and sighted users can see where they are.
|
||||
*/
|
||||
|
||||
type NavItem = {
|
||||
@@ -20,13 +24,20 @@ type NavItem = {
|
||||
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{ href: "/", label: "בית" },
|
||||
{ href: "/upload", label: "העלאת מסמכים" },
|
||||
{ href: "/cases/new", label: "תיק חדש" },
|
||||
{ href: "/training", label: "אימון סגנון" },
|
||||
{ href: "/skills", label: "מיומנויות" },
|
||||
{ href: "/diagnostics", label: "אבחון" },
|
||||
];
|
||||
|
||||
function isActive(pathname: string, href: string): boolean {
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
export function AppShell({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
@@ -45,25 +56,43 @@ export function AppShell({ children }: { children: ReactNode }) {
|
||||
<span className="text-gold-soft text-sm font-medium">ניהול תיקים</span>
|
||||
</Link>
|
||||
|
||||
<nav className="me-auto flex items-center gap-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<nav
|
||||
className="me-auto flex items-center gap-1"
|
||||
aria-label="ניווט ראשי"
|
||||
>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const active = isActive(pathname, item.href);
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="
|
||||
px-3 py-1.5 rounded
|
||||
text-sm text-parchment/80
|
||||
transition-colors
|
||||
hover:text-parchment hover:bg-navy-soft/60
|
||||
"
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`
|
||||
relative px-3 py-1.5 rounded text-sm transition-colors
|
||||
${
|
||||
active
|
||||
? "text-parchment font-semibold bg-navy-soft/80"
|
||||
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.label}
|
||||
{active && (
|
||||
<span
|
||||
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10">
|
||||
<main
|
||||
id="main"
|
||||
className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10"
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -79,9 +79,10 @@ function Row({ item }: { item: CorpusDecision }) {
|
||||
size="sm"
|
||||
onClick={onDelete}
|
||||
disabled={del.isPending}
|
||||
aria-label={`הסר את ${item.decision_number || "החלטה זו"} מהקורפוס`}
|
||||
className="text-danger hover:text-danger hover:bg-danger-bg"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -79,8 +79,14 @@ export function PartiesField({
|
||||
placeholder="שם מלא של הצד"
|
||||
dir="rtl"
|
||||
/>
|
||||
<Button type="button" variant="outline" size="sm" onClick={add}>
|
||||
<Plus className="w-4 h-4" />
|
||||
<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>}
|
||||
|
||||
Reference in New Issue
Block a user