@@ -127,12 +130,6 @@ export function FileGrid({
Download
- {file.mimeType && PREVIEWABLE_MIMES.has(file.mimeType) && (
- onPreview(file)}>
-
- Preview
-
- )}
onRename(file)}>
Rename
diff --git a/src/components/interests/interest-eoi-tab.tsx b/src/components/interests/interest-eoi-tab.tsx
index 79d5853e..a7c63a35 100644
--- a/src/components/interests/interest-eoi-tab.tsx
+++ b/src/components/interests/interest-eoi-tab.tsx
@@ -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) {
setGenerateOpen(true)}
onUploadSigned={() => setUploadSignedOpen(true)}
+ onMarkSigned={() => setMarkSignedOpen(true)}
/>
)}
@@ -197,6 +200,13 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
interestId={interestId}
/>
+
+
{
@@ -559,9 +569,11 @@ function SignedPdfPreview({ fileId }: { fileId: string }) {
function EmptyEoiState({
onGenerate,
onUploadSigned,
+ onMarkSigned,
}: {
onGenerate: () => void;
onUploadSigned: () => void;
+ onMarkSigned: () => void;
}) {
return (
@@ -572,7 +584,7 @@ function EmptyEoiState({
No EOI in flight for this interest
- 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.
@@ -584,6 +596,10 @@ function EmptyEoiState({
Upload paper-signed copy
+
);
diff --git a/src/components/ui/field-error.tsx b/src/components/ui/field-error.tsx
new file mode 100644
index 00000000..d6e68a52
--- /dev/null
+++ b/src/components/ui/field-error.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+export interface FieldErrorProps extends React.HTMLAttributes {
+ /**
+ * 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 (
+
+ {message ?? ''}
+
+ );
+}
diff --git a/src/components/ui/field-label.tsx b/src/components/ui/field-label.tsx
new file mode 100644
index 00000000..88ca6b3b
--- /dev/null
+++ b/src/components/ui/field-label.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import * as React from 'react';
+import { Info } from 'lucide-react';
+
+import { Label } from '@/components/ui/label';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
+import { cn } from '@/lib/utils';
+
+export interface FieldLabelProps extends React.ComponentPropsWithoutRef {
+ /**
+ * Optional explainer copy. When set, an Info icon is rendered next
+ * to the label that opens a Tooltip on hover / focus. Tooltip writing
+ * guidelines: 1-2 sentences max; state the unit ("...in days",
+ * "...in MB"); lead with the risk for dangerous settings; explain the
+ * "why and how to choose a value" rather than restating the label.
+ */
+ tooltip?: React.ReactNode;
+ /** Mark the field as required — appends an asterisk after the label text. */
+ required?: boolean;
+}
+
+/**
+ * Shared label-with-explainer primitive for admin forms. Wraps the
+ * shadcn Label + an optional Info-tooltip slot. Adopting this everywhere
+ * an admin setting needs a non-obvious explainer (Sort Order, Weight,
+ * TTL, Cap, Confidence, Threshold) gives admins consistent tooltip UX
+ * without per-form bespoke implementations.
+ */
+export function FieldLabel({
+ tooltip,
+ required,
+ className,
+ children,
+ ...labelProps
+}: FieldLabelProps) {
+ if (!tooltip) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}