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

@@ -25,6 +25,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { EoiCancelDialog } from '@/components/documents/eoi-cancel-dialog';
import { EoiGenerateDialog } from '@/components/documents/eoi-generate-dialog';
import { ExternalEoiUploadDialog } from '@/components/interests/external-eoi-upload-dialog';
import { MarkExternallySignedDialog } from '@/components/interests/mark-externally-signed-dialog';
import { SigningProgress } from '@/components/documents/signing-progress';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
import { apiFetch } from '@/lib/api/client';
@@ -104,6 +105,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
const portSlug = useUIStore((s) => s.currentPortSlug);
const [generateOpen, setGenerateOpen] = useState(false);
const [uploadSignedOpen, setUploadSignedOpen] = useState(false);
const [markSignedOpen, setMarkSignedOpen] = useState(false);
// Lifted preview state so the View button on every signed-PDF row opens
// the in-app preview dialog rather than navigating to a presigned URL
// (which the storage backend serves with Content-Disposition=attachment,
@@ -137,6 +139,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
<EmptyEoiState
onGenerate={() => setGenerateOpen(true)}
onUploadSigned={() => setUploadSignedOpen(true)}
onMarkSigned={() => setMarkSignedOpen(true)}
/>
)}
@@ -197,6 +200,13 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
interestId={interestId}
/>
<MarkExternallySignedDialog
open={markSignedOpen}
onOpenChange={setMarkSignedOpen}
interestId={interestId}
docType="eoi"
/>
<FilePreviewDialog
open={!!previewFile}
onOpenChange={(o) => {
@@ -559,9 +569,11 @@ function SignedPdfPreview({ fileId }: { fileId: string }) {
function EmptyEoiState({
onGenerate,
onUploadSigned,
onMarkSigned,
}: {
onGenerate: () => void;
onUploadSigned: () => void;
onMarkSigned: () => void;
}) {
return (
<section className="rounded-xl border border-dashed bg-muted/20 p-8 text-center">
@@ -572,7 +584,7 @@ function EmptyEoiState({
No EOI in flight for this interest
</h2>
<p className="mt-1 text-sm text-muted-foreground">
Generate the EOI to send it for signing the signing service handles the signing chain. You
Generate the EOI to send it for signing. The signing service handles the signing chain. You
can also upload a paper-signed copy if it was signed outside the system.
</p>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
@@ -584,6 +596,10 @@ function EmptyEoiState({
<Upload className="size-4" aria-hidden />
Upload paper-signed copy
</Button>
<Button onClick={onMarkSigned} variant="ghost" size="sm" className="gap-1.5">
<CheckCircle2 className="size-4" aria-hidden />
Mark signed without file
</Button>
</div>
</section>
);