feat(uat-batch-4): a11y form primitives + click-to-preview + EOI empty-state + lint guards

- FieldError primitive (role=alert, aria-live) — used by Wave 3
  form-error UX work.
- FieldLabel primitive (Label + Info-tooltip slot) — foundational for
  the platform-wide admin-settings tooltip audit.
- ESLint guard against em-dash in user-facing JSX text inside
  src/components + src/app (warning, not error; 111 existing instances
  flagged for follow-up sweep).
- FileGrid card body becomes click-to-preview button (was hidden under
  a kebab); aria-label per row; kebab keeps Download/Rename/Delete.
- DocumentList: title cell on rows with signedFileId opens
  FilePreviewDialog; kebab gains Download action (was missing
  per UAT). Single FilePreviewDialog instance lifted to the parent.
- DocumentList type extended with signedFileId.
- EOI empty state: third ghost button "Mark signed without file"
  wired to existing MarkExternallySignedDialog (parity with
  reservation tab).
- Watcher empty-state padding fix on document-detail.

tsc clean. 1419/1419 vitest. lint clean on touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 17:20:13 +02:00
parent 6a4f4ea1dd
commit 52342ee45d
7 changed files with 212 additions and 17 deletions

View File

@@ -0,0 +1,48 @@
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface FieldErrorProps extends React.HTMLAttributes<HTMLParagraphElement> {
/**
* The error message to render. When falsy the component renders an
* empty live region so a screen reader still picks up subsequent
* error messages without an aria-live remount.
*/
message?: string | null | undefined;
/**
* Optional id — pair with `aria-describedby` on the associated input
* so SR users hear the error after the input's accessible name.
*/
id?: string;
}
/**
* Accessible error renderer for form fields. Always renders a
* `role="alert"` + `aria-live="polite"` region so SR users get
* immediate feedback when validation fires. Hide visually when
* `message` is empty without removing the region from the DOM —
* tearing the live region down between submits delays the next
* announcement on most assistive tech.
*
* Caller is responsible for setting `aria-invalid` and
* `aria-describedby={id}` on the linked input.
*/
export function FieldError({ message, id, className, ...props }: FieldErrorProps) {
return (
<p
id={id}
role="alert"
aria-live="polite"
className={cn(
'text-xs text-destructive',
!message && 'sr-only', // empty state keeps the region for next message
className,
)}
{...props}
>
{message ?? ''}
</p>
);
}