fix(frontend): design-review polish — table-compact height, header type-scale, tabs a11y

Fix 1 (A): move row height constraint from min-height on tbody tr
(CSS no-op on table-row) to height: 32px on td (works on table-cell).
Fix 2 (A): table header text-[11px] -> text-[12px] to align with
documented caption scale.
Fix 3: add aria-controls + id to Tabs buttons; wrap active tab content
in role="tabpanel" + aria-labelledby in EngagementDetailPage.
Test: add aria-controls/id assertion to Tabs.test.tsx (234 total).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-21 22:18:09 +02:00
parent 1324a0c8ce
commit 790ced4204
4 changed files with 46 additions and 28 deletions

View File

@@ -21,8 +21,10 @@ export function Tabs({ items, activeId, onChange }: TabsProps): JSX.Element {
return ( return (
<button <button
key={item.id} key={item.id}
id={`tab-${item.id}`}
role="tab" role="tab"
aria-selected={isActive} aria-selected={isActive}
aria-controls={`tabpanel-${item.id}`}
type="button" type="button"
onClick={() => onChange(item.id)} onClick={() => onChange(item.id)}
className={`tab-underline${isActive ? ' tab-underline-active' : ''} pb-xs`} className={`tab-underline${isActive ? ' tab-underline-active' : ''} pb-xs`}

View File

@@ -68,34 +68,40 @@ export function EngagementDetailPage(): JSX.Element {
<Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} /> <Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} />
{activeTab === 'schedule' && ( <div
<div className="card-product"> role="tabpanel"
<h2 className="text-[20px] font-medium mb-md">Schedule</h2> id={`tabpanel-${activeTab}`}
<dl className="grid grid-cols-2 gap-md text-[14px]"> aria-labelledby={`tab-${activeTab}`}
<dt className="text-graphite">Start date</dt> >
<dd className="text-ink font-mono">{eng.start_date}</dd> {activeTab === 'schedule' && (
<dt className="text-graphite">End date</dt> <div className="card-product">
<dd className="text-ink font-mono">{eng.end_date ?? '—'}</dd> <h2 className="text-[20px] font-medium mb-md">Schedule</h2>
<dt className="text-graphite">Status</dt> <dl className="grid grid-cols-2 gap-md text-[14px]">
<dd className="text-ink capitalize">{eng.status}</dd> <dt className="text-graphite">Start date</dt>
<dt className="text-graphite">Created at</dt> <dd className="text-ink font-mono">{eng.start_date}</dd>
<dd className="text-ink font-mono">{eng.created_at}</dd> <dt className="text-graphite">End date</dt>
</dl> <dd className="text-ink font-mono">{eng.end_date ?? '—'}</dd>
</div> <dt className="text-graphite">Status</dt>
)} <dd className="text-ink capitalize">{eng.status}</dd>
<dt className="text-graphite">Created at</dt>
<dd className="text-ink font-mono">{eng.created_at}</dd>
</dl>
</div>
)}
{activeTab === 'description' && ( {activeTab === 'description' && (
<div className="card-product"> <div className="card-product">
<h2 className="text-[20px] font-medium mb-md">Description</h2> <h2 className="text-[20px] font-medium mb-md">Description</h2>
<p className="text-[16px] text-charcoal whitespace-pre-line"> <p className="text-[16px] text-charcoal whitespace-pre-line">
{eng.description?.trim() ? eng.description : 'No description provided.'} {eng.description?.trim() ? eng.description : 'No description provided.'}
</p> </p>
</div> </div>
)} )}
{activeTab === 'simulations' && ( {activeTab === 'simulations' && (
<SimulationList engagementId={eng.id} /> <SimulationList engagementId={eng.id} />
)} )}
</div>
</div> </div>
); );
} }

View File

@@ -179,16 +179,16 @@
/* ─── Compact table density (L4) — 32px row height, global ──────────── */ /* ─── Compact table density (L4) — 32px row height, global ──────────── */
.table-compact thead tr { .table-compact thead tr {
@apply text-[11px] uppercase tracking-[0.5px] text-graphite; @apply text-[12px] uppercase tracking-[0.5px] text-graphite;
} }
.table-compact th { .table-compact th {
@apply px-md py-xxs; @apply px-md py-xxs;
} }
.table-compact td { .table-compact td {
@apply px-md py-xxs caption-md; @apply px-md py-xxs caption-md;
height: 32px;
} }
.table-compact tbody tr { .table-compact tbody tr {
@apply border-b border-hairline last:border-0; @apply border-b border-hairline last:border-0;
min-height: 32px;
} }
} }

View File

@@ -49,6 +49,16 @@ describe('Tabs', () => {
expect(onChange).toHaveBeenCalledWith('description'); expect(onChange).toHaveBeenCalledWith('description');
}); });
it('sets aria-controls and id on each tab button', () => {
render(<Tabs items={ITEMS} activeId="schedule" onChange={vi.fn()} />);
const schedBtn = screen.getByRole('tab', { name: /Schedule/i });
expect(schedBtn).toHaveAttribute('id', 'tab-schedule');
expect(schedBtn).toHaveAttribute('aria-controls', 'tabpanel-schedule');
const simsBtn = screen.getByRole('tab', { name: /Simulations/i });
expect(simsBtn).toHaveAttribute('id', 'tab-simulations');
expect(simsBtn).toHaveAttribute('aria-controls', 'tabpanel-simulations');
});
it('has no rounded-md, transition-*, or shadow-* on tab buttons (brutalism)', () => { it('has no rounded-md, transition-*, or shadow-* on tab buttons (brutalism)', () => {
render(<Tabs items={ITEMS} activeId="schedule" onChange={vi.fn()} />); render(<Tabs items={ITEMS} activeId="schedule" onChange={vi.fn()} />);
for (const btn of screen.getAllByRole('tab')) { for (const btn of screen.getAllByRole('tab')) {