fix(frontend): post-design-review — slab token split + badge contrast + modal backdrop + dark shadows

- Add fixed slab/slab-text/slab-muted tokens so utility strip and footer never
  invert to near-white in dark mode (root token split: ink is themed text,
  slab is fixed dark surface)
- btn-ink uses fixed #111827 so confirm dialogs stay dark-on-dark readable
- Toast error surface switched to slab; success uses text-white (not text-ink-on)
- StatusBadge active and SimulationStatusBadge review_required/done use text-white
  instead of text-canvas/text-ink-on (prevents near-black text on colored pill
  in dark mode)
- Modal backdrops (MitreMatrixModal, ConfirmDialog) switched to .modal-backdrop
  class (fixed rgba(0,0,0,0.6)) instead of bg-ink/60 which turned near-white
- Card shadow lifted in dark mode via .dark .card-product override
- MitreMatrixModal panel uses shadow-floating-dark in dark mode
- UsersAdminPage form: items-start + explicit label-height spacer on button
  column for pixel-perfect baseline alignment (AC-17.3 structural fix)

92/92 tests passing, typecheck and lint clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-05-27 20:19:16 +02:00
parent f5ea9d16af
commit 892692f3b8
10 changed files with 46 additions and 21 deletions

View File

@@ -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}

View File

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

View File

@@ -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}
> >

View File

@@ -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 {

View File

@@ -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',
}; };

View File

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

View File

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

View File

@@ -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;

View File

@@ -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',

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); if (backdrop) await user.click(backdrop);
expect(onCancel).toHaveBeenCalled(); expect(onCancel).toHaveBeenCalled();