refactor(ara-projects): make the add-document composer read as a form (#412)

Labelled cells sitting directly under the table's own headings read as a
second header row: two near-identical sets of words that never quite
lined up, worst of all when the table was empty and they were the only
two things on screen.

The composer now announces itself — a thick divider, its own "Add a
document" heading — and lays its controls out as a form rather than as
twelfth-aligned columns: placeholders and aria-labels instead of column
captions, fields free to size themselves, a text "Add" button. The empty
table points at the heading by name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Roth 2026-07-23 13:57:04 +00:00
parent feff631582
commit 5c94a538ed

View file

@ -214,8 +214,9 @@ export function RequirementsTable({
data-testid="evidence-empty"
>
No documents required yet. This workstream is still complete
without any add one below if the contractor should be asked
for something.
without any use <strong className="font-semibold">Add a
document</strong> below if the contractor should be asked for
something.
</td>
</tr>
)}
@ -314,9 +315,18 @@ export function RequirementsTable({
</table>
</div>
{/*
The composer is deliberately *not* a twelfth column-aligned row.
Labelled cells sitting under the table's own headings read as a second
header row two near-identical sets of words, never quite in line,
which is at its worst when the table is empty. So it announces itself
with its own heading and lays its controls out as a form: the fields
carry placeholders and `aria-label`s rather than column captions, and
they are free to size themselves.
*/}
{canManage && (
<form
className="border-t border-gray-200 bg-gray-50/70 px-5 py-4"
className="border-t-4 border-gray-100 bg-gray-50/70 px-5 py-4"
data-testid="evidence-add-form"
onSubmit={(event) => {
event.preventDefault();
@ -327,125 +337,99 @@ export function RequirementsTable({
add.mutate(draft);
}}
>
{/*
Grid children default to `min-width: auto`, so a cell narrower than
its contents spills over its neighbour rather than shrinking. Every
cell below is `min-w-0` for that reason, and the required toggle
gets two columns a checkbox and the word "Required" do not fit in
one twelfth of the row.
*/}
<div className="grid grid-cols-1 items-end gap-3 md:grid-cols-12">
<label className="min-w-0 md:col-span-3">
<span className="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-400">
Document
</span>
<select
className={selectClass}
value={draft.fileType}
disabled={busy}
data-testid="evidence-add-file-type"
onChange={(event) =>
setDraft({ ...draft, fileType: event.target.value })
}
>
<option value="">Choose a document</option>
{FILE_TYPE_GROUPS.map((group) => (
<optgroup key={group.group} label={group.group}>
{group.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
))}
</select>
</label>
<p className="mb-2.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-gray-500">
<Plus className="h-3.5 w-3.5" />
Add a document
</p>
<label className="min-w-0 md:col-span-3">
<span className="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-400">
Stage <span className="normal-case text-gray-300">optional</span>
</span>
<select
className={selectClass}
value={draft.projectWorkstreamStageId}
disabled={busy || !hasStages}
data-testid="evidence-add-stage"
onChange={(event) =>
setDraft({
...draft,
projectWorkstreamStageId: event.target.value,
})
}
>
<option value={NO_STAGE}>
{hasStages ? "Whole workstream" : "No stages yet"}
<div className="flex flex-wrap items-center gap-3">
<select
className={`${selectClass} min-w-[11rem] flex-[2]`}
value={draft.fileType}
disabled={busy}
aria-label="Document"
data-testid="evidence-add-file-type"
onChange={(event) =>
setDraft({ ...draft, fileType: event.target.value })
}
>
<option value="">Choose a document</option>
{FILE_TYPE_GROUPS.map((group) => (
<optgroup key={group.group} label={group.group}>
{group.options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</optgroup>
))}
</select>
<select
className={`${selectClass} min-w-[10rem] flex-1`}
value={draft.projectWorkstreamStageId}
disabled={busy || !hasStages}
aria-label="Stage"
data-testid="evidence-add-stage"
onChange={(event) =>
setDraft({
...draft,
projectWorkstreamStageId: event.target.value,
})
}
>
<option value={NO_STAGE}>
{hasStages ? "No stage — whole workstream" : "No stages yet"}
</option>
{workstream.stages.map((stage) => (
<option key={stage.id} value={stage.id}>
{stage.name}
</option>
{workstream.stages.map((stage) => (
<option key={stage.id} value={stage.id}>
{stage.name}
</option>
))}
</select>
</label>
))}
</select>
<label className="min-w-0 md:col-span-3">
<span className="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-400">
Naming rule{" "}
<span className="normal-case text-gray-300">optional</span>
</span>
<input
type="text"
className={`${inputClass} min-w-[10rem] flex-1`}
value={draft.namingRule}
disabled={busy}
placeholder="Naming rule, e.g. [WORef]_Cert"
aria-label="Naming rule"
data-testid="evidence-add-naming-rule"
onChange={(event) =>
setDraft({ ...draft, namingRule: event.target.value })
}
/>
<label className="flex items-center gap-2 whitespace-nowrap text-xs text-gray-600">
<input
type="text"
className={inputClass}
value={draft.namingRule}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
checked={draft.required}
disabled={busy}
placeholder="[AssetRef]_[WORef]_Cert"
data-testid="evidence-add-naming-rule"
data-testid="evidence-add-required"
onChange={(event) =>
setDraft({ ...draft, namingRule: event.target.value })
setDraft({ ...draft, required: event.target.checked })
}
/>
Required
</label>
<label className="min-w-0 md:col-span-2">
<span className="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-400 md:text-center">
Required
</span>
<span className="flex h-9 items-center gap-2 md:justify-center">
<input
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
checked={draft.required}
disabled={busy}
data-testid="evidence-add-required"
onChange={(event) =>
setDraft({ ...draft, required: event.target.checked })
}
/>
<span className="text-xs text-gray-500 md:hidden">
Contractors must supply this
</span>
</span>
</label>
<div className="min-w-0 md:col-span-1">
<Button
type="submit"
size="sm"
disabled={busy}
className="w-full"
data-testid="evidence-add-submit"
>
<Plus className="h-4 w-4" />
<span className="ml-1.5 md:sr-only md:ml-0">Add</span>
</Button>
</div>
<Button
type="submit"
size="sm"
disabled={busy}
data-testid="evidence-add-submit"
>
Add
</Button>
</div>
{!hasStages && (
<p className="mt-2 text-xs text-gray-400">
This workstream has no stages yet, so a requirement applies to the
workstream as a whole. Tag it with a stage once the ladder exists.
</p>
)}
<p className="mt-2 text-xs text-gray-400">
{hasStages
? "Leave the stage unset to expect the document at any point in the workstream."
: "This workstream has no stages yet, so a requirement applies to the workstream as a whole. Tag it with a stage once the ladder exists."}
</p>
</form>
)}