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>
</div> </div>
{/* nav-bar-top — canvas with hairline */} {/* nav-bar-top — paper gives dark-mode lift vs canvas body */}
<header className="bg-canvas border-b border-hairline"> <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"> <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"> <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 /> <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 { Link } from 'react-router-dom';
import { Plus } from 'lucide-react';
import { extractApiError } from '@/api/client'; import { extractApiError } from '@/api/client';
import type { Engagement } from '@/api/types'; import type { Engagement } from '@/api/types';
import { useDeleteEngagement, useEngagementsList } from '@/hooks/useEngagements'; import { useDeleteEngagement, useEngagementsList } from '@/hooks/useEngagements';
@@ -41,7 +42,7 @@ export function EngagementsListPage(): JSX.Element {
</div> </div>
{canEditEngagements ? ( {canEditEngagements ? (
<Link to="/engagements/new" className="btn-primary"> <Link to="/engagements/new" className="btn-primary">
+ New <Plus size={14} aria-hidden /> New
</Link> </Link>
) : null} ) : null}
</header> </header>
@@ -59,7 +60,7 @@ export function EngagementsListPage(): JSX.Element {
action={ action={
canEditEngagements ? ( canEditEngagements ? (
<Link to="/engagements/new" className="btn-primary"> <Link to="/engagements/new" className="btn-primary">
+ New engagement <Plus size={14} aria-hidden /> New engagement
</Link> </Link>
) : undefined ) : undefined
} }

View File

@@ -111,19 +111,33 @@ export function UsersAdminPage(): JSX.Element {
<section className="card-product flex flex-col gap-md"> <section className="card-product flex flex-col gap-md">
<h2 className="text-[20px] font-medium">Create account</h2> <h2 className="text-[20px] font-medium">Create account</h2>
{/* {/*
items-start so each cell top-aligns; the button is wrapped in a flex column Option A structural fix (AC-17.3): labels / inputs / hints in 3 explicit grid rows
that pushes it to align with the input row (label + 4px gap = ~22px offset). 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"> <form
<FormField label="Username" htmlFor="new-username" required> 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 <TextInput
id="new-username" id="new-username"
value={createForm.username} value={createForm.username}
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })} onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
required required
/> />
</FormField>
<FormField label="Password" htmlFor="new-password" required hint="≥ 8 characters">
<TextInput <TextInput
id="new-password" id="new-password"
type="password" type="password"
@@ -132,22 +146,21 @@ export function UsersAdminPage(): JSX.Element {
required required
minLength={8} minLength={8}
/> />
</FormField>
<FormField label="Role" htmlFor="new-role" required>
<Select <Select
id="new-role" id="new-role"
value={createForm.role} value={createForm.role}
onChange={(e) => setCreateForm({ ...createForm, role: e.target.value as Role })} onChange={(e) => setCreateForm({ ...createForm, role: e.target.value as Role })}
options={ROLE_OPTIONS} 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}> <button type="submit" className="btn-primary w-full" disabled={createMutation.isPending}>
{createMutation.isPending ? 'Creating…' : 'Create'} {createMutation.isPending ? 'Creating…' : 'Create'}
</button> </button>
</div>
{/* 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> </form>
{createError ? ( {createError ? (
<div role="alert" className="text-[14px] text-bloom-deep"> <div role="alert" className="text-[14px] text-bloom-deep">

View File

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