fix(frontend): sprint 4 design-review — slab token + UsersAdmin alignment + dark hairlines + badge contrast

- bump dark hairline from #374151 → #4b5563 for visible table borders
- topbar header bg-canvas → bg-paper for dark-mode lift vs canvas body
- UsersAdminPage create-form: Option A structural 3-row grid (labels / inputs / hints)
  to fix AC-17.3 alignment; removes FormField wrapper that caused row-height misalignment
- EngagementsListPage: replace text "+ New" with lucide Plus icon per design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-27 20:28:32 +02:00
parent 892692f3b8
commit 9964d058f4
4 changed files with 55 additions and 41 deletions

View File

@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
import { Plus } from 'lucide-react';
import { extractApiError } from '@/api/client';
import type { Engagement } from '@/api/types';
import { useDeleteEngagement, useEngagementsList } from '@/hooks/useEngagements';
@@ -41,7 +42,7 @@ export function EngagementsListPage(): JSX.Element {
</div>
{canEditEngagements ? (
<Link to="/engagements/new" className="btn-primary">
+ New
<Plus size={14} aria-hidden /> New
</Link>
) : null}
</header>
@@ -59,7 +60,7 @@ export function EngagementsListPage(): JSX.Element {
action={
canEditEngagements ? (
<Link to="/engagements/new" className="btn-primary">
+ New engagement
<Plus size={14} aria-hidden /> New engagement
</Link>
) : undefined
}

View File

@@ -111,43 +111,56 @@ export function UsersAdminPage(): JSX.Element {
<section className="card-product flex flex-col gap-md">
<h2 className="text-[20px] font-medium">Create account</h2>
{/*
items-start so each cell top-aligns; the button is wrapped in a flex column
that pushes it to align with the input row (label + 4px gap = ~22px offset).
Option A structural fix (AC-17.3): labels / inputs / hints in 3 explicit grid rows
so the browser can never misalign them by collapsing different-height cells.
grid-rows-[auto_auto_auto] ensures row 1 = labels, row 2 = inputs, row 3 = hints.
*/}
<form onSubmit={onCreate} className="grid grid-cols-1 md:grid-cols-4 gap-md items-start">
<FormField label="Username" htmlFor="new-username" required>
<TextInput
id="new-username"
value={createForm.username}
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
required
/>
</FormField>
<FormField label="Password" htmlFor="new-password" required hint="≥ 8 characters">
<TextInput
id="new-password"
type="password"
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
required
minLength={8}
/>
</FormField>
<FormField label="Role" htmlFor="new-role" required>
<Select
id="new-role"
value={createForm.role}
onChange={(e) => setCreateForm({ ...createForm, role: e.target.value as Role })}
options={ROLE_OPTIONS}
/>
</FormField>
{/* Button column: spacer matches label row height so input + button baselines align */}
<div className="flex flex-col gap-xs">
<div className="h-[22px]" aria-hidden="true" />
<button type="submit" className="btn-primary w-full" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating…' : 'Create'}
</button>
</div>
<form
onSubmit={onCreate}
className="grid grid-cols-1 md:grid-cols-4 md:grid-rows-[auto_auto_auto] gap-x-md gap-y-xs"
>
{/* Row 1 — labels */}
<label htmlFor="new-username" className="text-[14px] font-medium text-ink">
Username <span className="text-bloom-deep">*</span>
</label>
<label htmlFor="new-password" className="text-[14px] font-medium text-ink">
Password <span className="text-bloom-deep">*</span>
</label>
<label htmlFor="new-role" className="text-[14px] font-medium text-ink">
Role <span className="text-bloom-deep">*</span>
</label>
<div aria-hidden="true" />
{/* Row 2 — inputs + button (all same height = h-11) */}
<TextInput
id="new-username"
value={createForm.username}
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
required
/>
<TextInput
id="new-password"
type="password"
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
required
minLength={8}
/>
<Select
id="new-role"
value={createForm.role}
onChange={(e) => setCreateForm({ ...createForm, role: e.target.value as Role })}
options={ROLE_OPTIONS}
/>
<button type="submit" className="btn-primary w-full" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating…' : 'Create'}
</button>
{/* Row 3 — hints */}
<div aria-hidden="true" />
<span className="text-[12px] text-graphite"> 8 characters</span>
<div aria-hidden="true" />
<div aria-hidden="true" />
</form>
{createError ? (
<div role="alert" className="text-[14px] text-bloom-deep">