feat: sprint 4 — UI polish + dark mode + workflow tightening + process hygiene #7
@@ -24,7 +24,7 @@ export function ConfirmDialog({
|
|||||||
aria-labelledby="confirm-dialog-title"
|
aria-labelledby="confirm-dialog-title"
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
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">
|
<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">
|
<h2 id="confirm-dialog-title" className="text-[20px] font-medium text-ink">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export function Layout(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full flex flex-col bg-canvas">
|
<div className="min-h-full flex flex-col bg-canvas">
|
||||||
{/* utility-strip — ink slab, fine print */}
|
{/* utility-strip — fixed dark slab, never inverts */}
|
||||||
<div className="bg-ink text-white text-[14px] h-9 flex items-center">
|
<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">
|
<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>
|
<span className="font-medium tracking-[0.5px] uppercase">Mimic · Purple Team BAS</span>
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center gap-md">
|
<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}
|
{user.role}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[14px]">{user.username}</span>
|
<span className="text-[14px]">{user.username}</span>
|
||||||
@@ -42,7 +42,7 @@ export function Layout(): JSX.Element {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={cycleTheme}
|
onClick={cycleTheme}
|
||||||
aria-label={`Theme: ${themeLabel(theme)} — click to cycle`}
|
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} />
|
<ThemeIcon theme={theme} />
|
||||||
<span className="uppercase tracking-[0.5px]">{themeLabel(theme)}</span>
|
<span className="uppercase tracking-[0.5px]">{themeLabel(theme)}</span>
|
||||||
@@ -101,9 +101,9 @@ export function Layout(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* footer — ink slab close */}
|
{/* footer — fixed dark slab, never inverts */}
|
||||||
<footer className="bg-ink text-white">
|
<footer className="bg-slab text-slab-text">
|
||||||
<div className="mx-auto w-full max-w-page px-xl py-xl text-[12px] text-steel">
|
<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.
|
Mimic — Internal Purple Team tooling. Authorized engagements only.
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -163,14 +163,14 @@ export function MitreMatrixModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<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
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="matrix-modal-title"
|
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' }}
|
style={{ width: '1400px' }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ const LABELS: Record<SimulationStatus, string> = {
|
|||||||
done: 'Done',
|
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> = {
|
const STYLES: Record<SimulationStatus, string> = {
|
||||||
pending: 'bg-fog text-charcoal border border-hairline',
|
pending: 'bg-fog text-charcoal border border-hairline',
|
||||||
in_progress: 'bg-primary-soft text-primary-deep',
|
in_progress: 'bg-primary-soft text-primary-deep',
|
||||||
review_required: 'bg-bloom-coral text-canvas',
|
review_required: 'bg-bloom-coral text-white',
|
||||||
done: 'bg-storm-deep text-canvas',
|
done: 'bg-storm-deep text-white',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element {
|
export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const STYLES: Record<EngagementStatus, string> = {
|
|||||||
// Outlined ink for planned (neutral), filled primary for active (engagement live),
|
// Outlined ink for planned (neutral), filled primary for active (engagement live),
|
||||||
// outlined steel for closed (muted). Stays within DESIGN.md palette.
|
// outlined steel for closed (muted). Stays within DESIGN.md palette.
|
||||||
planned: 'bg-canvas text-ink border border-ink',
|
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',
|
closed: 'bg-cloud text-graphite border border-hairline',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,11 @@ export function ToastViewport(): JSX.Element {
|
|||||||
{toasts.map((t) => {
|
{toasts.map((t) => {
|
||||||
const isError = t.kind === 'error';
|
const isError = t.kind === 'error';
|
||||||
const isSuccess = t.kind === 'success';
|
const isSuccess = t.kind === 'success';
|
||||||
|
// Fixed colors: toasts don't theme (error=dark slab, success=primary blue)
|
||||||
const surface = isError
|
const surface = isError
|
||||||
? 'bg-ink text-ink-on'
|
? 'bg-slab text-slab-text'
|
||||||
: isSuccess
|
: isSuccess
|
||||||
? 'bg-primary text-ink-on'
|
? 'bg-primary text-white'
|
||||||
: 'bg-canvas text-ink border border-hairline';
|
: 'bg-canvas text-ink border border-hairline';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -110,7 +110,11 @@ 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>
|
||||||
<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>
|
<FormField label="Username" htmlFor="new-username" required>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="new-username"
|
id="new-username"
|
||||||
@@ -137,7 +141,9 @@ export function UsersAdminPage(): JSX.Element {
|
|||||||
options={ROLE_OPTIONS}
|
options={ROLE_OPTIONS}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</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}>
|
<button type="submit" className="btn-primary w-full" disabled={createMutation.isPending}>
|
||||||
{createMutation.isPending ? 'Creating…' : 'Create'}
|
{createMutation.isPending ? 'Creating…' : 'Create'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
* DESIGN.md component recipes.
|
* DESIGN.md component recipes.
|
||||||
* Buttons stay sharp (rounded-md = 4px); cards stay soft (rounded-xl = 16px).
|
* Buttons stay sharp (rounded-md = 4px); cards stay soft (rounded-xl = 16px).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.btn-primary {
|
.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;
|
@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;
|
@apply bg-steel cursor-not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* btn-ink uses fixed dark slab so it doesn't invert in dark mode */
|
||||||
.btn-ink {
|
.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 {
|
.btn-ink:hover {
|
||||||
@apply bg-ink-soft;
|
background-color: #1f2937;
|
||||||
}
|
}
|
||||||
.btn-ink:disabled {
|
.btn-ink:disabled {
|
||||||
@apply bg-steel cursor-not-allowed;
|
@apply bg-steel cursor-not-allowed;
|
||||||
@@ -115,6 +118,14 @@
|
|||||||
.card-product {
|
.card-product {
|
||||||
@apply bg-canvas rounded-xl p-xl shadow-soft-lift;
|
@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 {
|
.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;
|
@apply inline-flex items-center bg-ink text-white rounded-lg px-3 py-[6px] text-[14px] leading-[1.3] font-medium;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
charcoal: 'var(--color-charcoal)',
|
charcoal: 'var(--color-charcoal)',
|
||||||
graphite: 'var(--color-graphite)',
|
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)
|
// Semantic / decorative — fixed (not themeable)
|
||||||
bloom: {
|
bloom: {
|
||||||
coral: '#ff5050',
|
coral: '#ff5050',
|
||||||
@@ -91,6 +95,8 @@ const config: Config = {
|
|||||||
boxShadow: {
|
boxShadow: {
|
||||||
'soft-lift': '0 2px 8px rgba(26, 26, 26, 0.08)',
|
'soft-lift': '0 2px 8px rgba(26, 26, 26, 0.08)',
|
||||||
floating: '0 8px 24px rgba(26, 26, 26, 0.12)',
|
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: {
|
maxWidth: {
|
||||||
page: '1366px',
|
page: '1366px',
|
||||||
|
|||||||
@@ -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);
|
if (backdrop) await user.click(backdrop);
|
||||||
|
|
||||||
expect(onCancel).toHaveBeenCalled();
|
expect(onCancel).toHaveBeenCalled();
|
||||||
|
|||||||
Reference in New Issue
Block a user