feat(frontend): Spectrum UX port — Tabs + AlertBanner + BackLink + compact tables (sprint 11) #13
@@ -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`}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export function EngagementDetailPage(): JSX.Element {
|
|||||||
|
|
||||||
<Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} />
|
<Tabs items={tabs} activeId={activeTab} onChange={setActiveTab} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
id={`tabpanel-${activeTab}`}
|
||||||
|
aria-labelledby={`tab-${activeTab}`}
|
||||||
|
>
|
||||||
{activeTab === 'schedule' && (
|
{activeTab === 'schedule' && (
|
||||||
<div className="card-product">
|
<div className="card-product">
|
||||||
<h2 className="text-[20px] font-medium mb-md">Schedule</h2>
|
<h2 className="text-[20px] font-medium mb-md">Schedule</h2>
|
||||||
@@ -97,5 +102,6 @@ export function EngagementDetailPage(): JSX.Element {
|
|||||||
<SimulationList engagementId={eng.id} />
|
<SimulationList engagementId={eng.id} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user