feat: sprint 4 — UI polish + dark mode + workflow tightening + process hygiene #7

Merged
knacky merged 15 commits from sprint/4-ui-polish into main 2026-05-28 04:01:21 +00:00
4 changed files with 55 additions and 41 deletions
Showing only changes of commit 9964d058f4 - Show all commits

View File

@@ -59,8 +59,8 @@ export function Layout(): JSX.Element {
</div>
</div>
{/* nav-bar-top — canvas with hairline */}
<header className="bg-canvas border-b border-hairline">
{/* nav-bar-top — paper gives dark-mode lift vs canvas body */}
<header className="bg-paper border-b border-hairline">
<div className="mx-auto w-full max-w-page px-xl h-16 flex items-center justify-between">
<Link to="/engagements" className="flex items-center gap-sm" aria-label="Mimic home">
<span className="inline-block h-6 w-6 rotate-12 bg-primary" aria-hidden />

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">

View File

@@ -31,7 +31,7 @@
--color-cloud: #1f2937;
--color-fog: #374151;
--color-steel: #4b5563;
--color-hairline: #374151;
--color-hairline: #4b5563;
--color-ink: #f9fafb;
--color-ink-soft: #e5e7eb;
--color-ink-deep: #ffffff;