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
10 changed files with 46 additions and 21 deletions
Showing only changes of commit 892692f3b8 - Show all commits

View File

@@ -24,7 +24,7 @@ export function ConfirmDialog({
aria-labelledby="confirm-dialog-title"
className="fixed inset-0 z-50 flex items-center justify-center"
>
<div className="absolute inset-0 bg-ink/40" onClick={onCancel} aria-hidden="true" />
<div className="modal-backdrop absolute inset-0" onClick={onCancel} aria-hidden="true" />
<div className="relative card-product shadow-floating max-w-sm w-full mx-md flex flex-col gap-md">
<h2 id="confirm-dialog-title" className="text-[20px] font-medium text-ink">
{title}

View File

@@ -28,13 +28,13 @@ export function Layout(): JSX.Element {
return (
<div className="min-h-full flex flex-col bg-canvas">
{/* utility-strip — ink slab, fine print */}
<div className="bg-ink text-white text-[14px] h-9 flex items-center">
{/* utility-strip — fixed dark slab, never inverts */}
<div className="bg-slab text-slab-text text-[14px] h-9 flex items-center">
<div className="mx-auto w-full max-w-page px-xl flex items-center justify-between">
<span className="font-medium tracking-[0.5px] uppercase">Mimic · Purple Team BAS</span>
{user ? (
<div className="flex items-center gap-md">
<span className="text-[12px] uppercase tracking-[0.5px] text-steel">
<span className="text-[12px] uppercase tracking-[0.5px] text-slab-muted">
{user.role}
</span>
<span className="text-[14px]">{user.username}</span>
@@ -42,7 +42,7 @@ export function Layout(): JSX.Element {
type="button"
onClick={cycleTheme}
aria-label={`Theme: ${themeLabel(theme)} — click to cycle`}
className="flex items-center gap-xxs text-[12px] text-steel hover:text-white transition-colors"
className="flex items-center gap-xxs text-[12px] text-slab-muted hover:text-slab-text transition-colors"
>
<ThemeIcon theme={theme} />
<span className="uppercase tracking-[0.5px]">{themeLabel(theme)}</span>
@@ -101,9 +101,9 @@ export function Layout(): JSX.Element {
</div>
</main>
{/* footer — ink slab close */}
<footer className="bg-ink text-white">
<div className="mx-auto w-full max-w-page px-xl py-xl text-[12px] text-steel">
{/* footer — fixed dark slab, never inverts */}
<footer className="bg-slab text-slab-text">
<div className="mx-auto w-full max-w-page px-xl py-xl text-[12px] text-slab-muted">
Mimic Internal Purple Team tooling. Authorized engagements only.
</div>
</footer>

View File

@@ -163,14 +163,14 @@ export function MitreMatrixModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="absolute inset-0 bg-ink/60" onClick={onCancel} aria-hidden="true" />
<div className="modal-backdrop absolute inset-0" onClick={onCancel} aria-hidden="true" />
<div
ref={containerRef}
role="dialog"
aria-modal="true"
aria-labelledby="matrix-modal-title"
className="relative bg-canvas rounded-xl shadow-floating max-w-[98vw] max-h-[80vh] overflow-hidden flex flex-col"
className="relative bg-canvas rounded-xl shadow-floating dark:shadow-floating-dark max-w-[98vw] max-h-[80vh] overflow-hidden flex flex-col"
style={{ width: '1400px' }}
onKeyDown={handleKeyDown}
>

View File

@@ -7,12 +7,13 @@ const LABELS: Record<SimulationStatus, string> = {
done: 'Done',
};
// pending=fog, in_progress=primary-soft, review_required=bloom-coral, done=storm-deep
// Fixed colors — badge backgrounds are decorative/semantic, not themeable.
// text-white is hardcoded (not text-canvas) so dark mode doesn't invert it to near-black.
const STYLES: Record<SimulationStatus, string> = {
pending: 'bg-fog text-charcoal border border-hairline',
in_progress: 'bg-primary-soft text-primary-deep',
review_required: 'bg-bloom-coral text-canvas',
done: 'bg-storm-deep text-canvas',
review_required: 'bg-bloom-coral text-white',
done: 'bg-storm-deep text-white',
};
export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element {

View File

@@ -10,7 +10,7 @@ const STYLES: Record<EngagementStatus, string> = {
// Outlined ink for planned (neutral), filled primary for active (engagement live),
// outlined steel for closed (muted). Stays within DESIGN.md palette.
planned: 'bg-canvas text-ink border border-ink',
active: 'bg-primary text-ink-on',
active: 'bg-primary text-white',
closed: 'bg-cloud text-graphite border border-hairline',
};

View File

@@ -15,10 +15,11 @@ export function ToastViewport(): JSX.Element {
{toasts.map((t) => {
const isError = t.kind === 'error';
const isSuccess = t.kind === 'success';
// Fixed colors: toasts don't theme (error=dark slab, success=primary blue)
const surface = isError
? 'bg-ink text-ink-on'
? 'bg-slab text-slab-text'
: isSuccess
? 'bg-primary text-ink-on'
? 'bg-primary text-white'
: 'bg-canvas text-ink border border-hairline';
return (
<div

View File

@@ -110,7 +110,11 @@ export function UsersAdminPage(): JSX.Element {
<section className="card-product flex flex-col gap-md">
<h2 className="text-[20px] font-medium">Create account</h2>
<form onSubmit={onCreate} className="grid grid-cols-1 md:grid-cols-4 gap-md items-end">
{/*
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).
*/}
<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"
@@ -137,7 +141,9 @@ export function UsersAdminPage(): JSX.Element {
options={ROLE_OPTIONS}
/>
</FormField>
<div>
{/* 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>

View File

@@ -67,6 +67,7 @@
* DESIGN.md component recipes.
* Buttons stay sharp (rounded-md = 4px); cards stay soft (rounded-xl = 16px).
*/
.btn-primary {
@apply inline-flex items-center justify-center gap-xs bg-primary text-white uppercase tracking-[0.7px] font-semibold text-[14px] leading-[1.4] rounded-md px-xl py-sm h-11 transition-colors;
}
@@ -77,11 +78,13 @@
@apply bg-steel cursor-not-allowed;
}
/* btn-ink uses fixed dark slab so it doesn't invert in dark mode */
.btn-ink {
@apply inline-flex items-center justify-center gap-xs bg-ink text-white uppercase tracking-[0.7px] font-semibold text-[14px] leading-[1.4] rounded-md px-xl py-sm h-11 transition-colors;
@apply inline-flex items-center justify-center gap-xs text-white uppercase tracking-[0.7px] font-semibold text-[14px] leading-[1.4] rounded-md px-xl py-sm h-11 transition-colors;
background-color: #111827;
}
.btn-ink:hover {
@apply bg-ink-soft;
background-color: #1f2937;
}
.btn-ink:disabled {
@apply bg-steel cursor-not-allowed;
@@ -115,6 +118,14 @@
.card-product {
@apply bg-canvas rounded-xl p-xl shadow-soft-lift;
}
.dark .card-product {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.32);
}
/* Fixed-color modal backdrop — must not use themed ink (inverts in dark mode) */
.modal-backdrop {
background-color: rgba(0, 0, 0, 0.6);
}
.badge-pill-ink {
@apply inline-flex items-center bg-ink text-white rounded-lg px-3 py-[6px] text-[14px] leading-[1.3] font-medium;

View File

@@ -34,6 +34,10 @@ const config: Config = {
},
charcoal: 'var(--color-charcoal)',
graphite: 'var(--color-graphite)',
// Fixed dark slab — never inverts in dark mode (utility strip, footer, dark bands)
slab: '#111827',
'slab-text': '#f9fafb',
'slab-muted': '#6b7280',
// Semantic / decorative — fixed (not themeable)
bloom: {
coral: '#ff5050',
@@ -91,6 +95,8 @@ const config: Config = {
boxShadow: {
'soft-lift': '0 2px 8px rgba(26, 26, 26, 0.08)',
floating: '0 8px 24px rgba(26, 26, 26, 0.12)',
'soft-lift-dark': '0 2px 8px rgba(0, 0, 0, 0.32)',
'floating-dark': '0 8px 24px rgba(0, 0, 0, 0.48)',
},
maxWidth: {
page: '1366px',

View File

@@ -306,7 +306,7 @@ describe('MitreMatrixModal', () => {
/>,
);
const backdrop = document.querySelector('.bg-ink\\/60') as HTMLElement;
const backdrop = document.querySelector('.modal-backdrop') as HTMLElement;
if (backdrop) await user.click(backdrop);
expect(onCancel).toHaveBeenCalled();