feat(design): terminal-SOC aesthetic refresh (sprint 7) #10

Merged
knacky merged 12 commits from sprint/7-design into main 2026-06-10 16:40:20 +00:00
15 changed files with 42 additions and 51 deletions
Showing only changes of commit ec7800ae38 - Show all commits

View File

@@ -25,7 +25,7 @@ export function ConfirmDialog({
className="fixed inset-0 z-50 flex items-center justify-center"
>
<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 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}
</h2>

View File

@@ -10,7 +10,7 @@ export function EmptyState({ title, description, action }: EmptyStateProps): JSX
return (
<div
data-testid="empty-state"
className="card-product flex flex-col items-start gap-md border border-hairline"
className="card-product flex flex-col items-start gap-md"
>
<h2 className="text-[24px] font-medium text-ink">{title}</h2>
{description ? <p className="text-[16px] text-charcoal">{description}</p> : null}

View File

@@ -9,7 +9,7 @@ export function ErrorState({ title = 'Something went wrong', message, onRetry }:
<div
role="alert"
data-testid="error-state"
className="card-product border border-bloom-deep/20 flex flex-col items-start gap-md"
className="card-product border-l-4 border-l-bloom-deep flex flex-col items-start gap-md"
>
<h2 className="text-[24px] font-medium text-bloom-deep">{title}</h2>
<p className="text-[16px] text-charcoal">{message}</p>

View File

@@ -54,7 +54,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
<div className="inline-flex">
<button
type="button"
className="btn-outline rounded-r-none border-r-0"
className="btn-outline border-r-0"
onClick={() => setOpen((v) => !v)}
data-testid="export-btn"
>
@@ -64,7 +64,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
type="button"
aria-label="Export options"
aria-expanded={open}
className="btn-outline rounded-l-none px-sm"
className="btn-outline px-sm"
onClick={() => setOpen((v) => !v)}
data-testid="export-dropdown-toggle"
>
@@ -74,7 +74,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
{open ? (
<div
className="absolute right-0 top-full mt-xxs bg-canvas border border-hairline rounded-md shadow-floating dark:shadow-floating-dark z-20 min-w-[160px]"
className="absolute right-0 top-full mt-xxs bg-canvas border border-hairline rounded-none z-20 min-w-[160px]"
role="menu"
>
{FORMATS.map(({ label, value }) => (

View File

@@ -34,15 +34,15 @@ export function Layout(): JSX.Element {
<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-slab-muted">
<span className="text-[12px] uppercase tracking-[0.5px] text-slab-muted font-mono">
{user.role}
</span>
<span className="text-[14px]">{user.username}</span>
<span className="text-[14px] font-mono">{user.username}</span>
<button
type="button"
onClick={cycleTheme}
aria-label={`Theme: ${themeLabel(theme)} — click to cycle`}
className="flex items-center gap-xxs text-[12px] text-slab-muted hover:text-slab-text transition-colors"
className="flex items-center gap-xxs text-[12px] text-slab-muted hover:text-slab-text"
>
<ThemeIcon theme={theme} />
<span className="uppercase tracking-[0.5px]">{themeLabel(theme)}</span>

View File

@@ -4,9 +4,9 @@ export function LoadingState({ label = 'Loading…' }: { label?: string }): JSX.
role="status"
aria-live="polite"
data-testid="loading-state"
className="flex items-center justify-center py-section text-graphite text-[16px]"
className="flex items-center justify-center py-section text-graphite text-[16px] font-mono"
>
<span className="inline-block h-2 w-2 rounded-pill bg-primary animate-pulse mr-sm" />
<span className="inline-block h-2 w-2 bg-primary animate-pulse mr-sm" />
{label}
</div>
);

View File

@@ -170,7 +170,7 @@ export function MitreMatrixModal({
role="dialog"
aria-modal="true"
aria-labelledby="matrix-modal-title"
className="relative bg-canvas rounded-xl shadow-floating dark:shadow-floating-dark max-w-[98vw] max-h-[80vh] overflow-hidden flex flex-col"
className="relative bg-canvas rounded-none border border-hairline max-w-[98vw] max-h-[80vh] overflow-hidden flex flex-col"
style={{ width: '1400px' }}
onKeyDown={handleKeyDown}
>
@@ -230,7 +230,7 @@ export function MitreMatrixModal({
type="button"
onClick={() => toggleTactic(tactic.tactic_id)}
title={`${tactic.tactic_name} (${tactic.tactic_id}) — click to tag this tactic`}
className={`w-full text-left px-xs py-xxs rounded-t-sm border border-b-0 border-hairline transition-colors ${
className={`w-full text-left px-xs py-xxs rounded-none border border-b-0 border-hairline ${
tacticSelected
? 'bg-primary border-primary'
: 'bg-cloud hover:bg-fog'
@@ -251,7 +251,7 @@ export function MitreMatrixModal({
</button>
{/* Techniques */}
<div className="border border-hairline rounded-b-sm overflow-hidden flex flex-col">
<div className="border border-hairline rounded-none overflow-hidden flex flex-col">
{visibleTechniques.map((tech, techIdx) => {
const isSelected = selectedTechMap.has(tech.id);
const isExpanded = expandedTechniques.has(tech.id) || autoExpanded.has(tech.id);
@@ -296,7 +296,7 @@ export function MitreMatrixModal({
isSelected ? 'text-white' : 'text-ink'
}`}
>
<span className="font-semibold block truncate">{tech.id}</span>
<span className="font-mono font-semibold block truncate">{tech.id}</span>
<span className={`block truncate text-[10px] ${isSelected ? 'text-white/80' : 'text-charcoal'}`}>
{tech.name}
</span>
@@ -318,7 +318,7 @@ export function MitreMatrixModal({
: 'bg-cloud text-charcoal hover:bg-fog'
}`}
>
<span className="font-semibold block truncate">{sub.id}</span>
<span className="font-mono font-semibold block truncate">{sub.id}</span>
<span className="block truncate">{sub.name}</span>
</button>
);

View File

@@ -107,7 +107,7 @@ export function MitreTechniquePicker({
/>
{open && (
<div className="absolute z-20 w-full mt-xxs bg-canvas border border-steel rounded-md shadow-floating overflow-hidden">
<div className="absolute z-20 w-full mt-xxs bg-canvas border border-steel rounded-none overflow-hidden">
{isFetching && (
<div className="px-md py-sm text-[14px] text-graphite">Searching</div>
)}
@@ -144,10 +144,10 @@ export function MitreTechniquePicker({
selectItem(item);
}}
>
<span className="font-medium">{item.id}</span>
<span className="font-mono font-medium">{item.id}</span>
<span className="text-charcoal"> {item.name}</span>
{item.tactics.length > 0 && (
<span className="text-graphite"> ({item.tactics[0]})</span>
<span className="font-mono text-graphite"> ({item.tactics[0]})</span>
)}
</li>
))}

View File

@@ -12,7 +12,7 @@ interface TacticTagProps {
disabled?: boolean;
}
// Technique chip — soft blue, id only, name in title
// Technique tag — angular, soft blue, monospace ID
export function MitreTechniqueTag({
technique,
onRemove,
@@ -22,7 +22,7 @@ export function MitreTechniqueTag({
<span
data-testid="mitre-technique-tag"
title={`${technique.id}${technique.name}`}
className="inline-flex items-center gap-xxs bg-primary-soft text-primary-deep rounded-full px-sm py-xxs text-[13px] font-medium"
className="inline-flex items-center gap-xxs bg-primary-soft text-primary-deep rounded-none border border-primary-soft px-sm py-xxs text-[13px] font-mono"
>
{technique.id}
{!disabled && (
@@ -39,7 +39,7 @@ export function MitreTechniqueTag({
);
}
// Tactic chip — primary blue filled, id only, name in title
// Tactic tag — angular, primary blue filled, monospace ID
export function MitreTacticTag({
tactic,
onRemove,
@@ -49,7 +49,7 @@ export function MitreTacticTag({
<span
data-testid="mitre-tactic-tag"
title={`${tactic.id}${tactic.name}`}
className="inline-flex items-center gap-xxs bg-primary text-white rounded-full px-sm py-xxs text-[13px] font-medium"
className="inline-flex items-center gap-xxs bg-primary text-white rounded-none px-sm py-xxs text-[13px] font-mono"
>
{tactic.id}
{!disabled && (

View File

@@ -116,7 +116,7 @@ export function MitreTechniquesField({
aria-label="Open MITRE matrix"
onClick={() => { setShowPicker(false); setShowMatrix(true); }}
disabled={isPending}
className="flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-md border border-steel text-graphite hover:text-ink hover:border-ink transition-colors"
className="flex-shrink-0 flex items-center justify-center w-9 h-9 rounded-none border border-steel text-graphite hover:text-ink hover:border-ink"
>
<Grid2x2 size={16} />
</button>

View File

@@ -73,7 +73,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
<div className="inline-flex">
<button
type="button"
className="btn-primary rounded-r-none border-r border-primary-deep"
className="btn-primary border-r border-primary-deep"
onClick={handleBlank}
data-testid="new-simulation-btn"
>
@@ -83,7 +83,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
type="button"
aria-label="More options"
aria-expanded={open}
className="btn-primary rounded-l-none px-sm"
className="btn-primary px-sm"
onClick={() => setOpen((v) => !v)}
data-testid="new-simulation-dropdown-toggle"
>
@@ -93,7 +93,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
{open ? (
<div
className="absolute right-0 top-full mt-xxs bg-canvas border border-hairline rounded-md shadow-floating dark:shadow-floating-dark z-20 min-w-[180px]"
className="absolute right-0 top-full mt-xxs bg-canvas border border-hairline rounded-none z-20 min-w-[180px]"
role="menu"
>
<button
@@ -199,7 +199,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
{sim.name}
</Link>
</td>
<td className="px-xl py-md text-charcoal text-[14px]">
<td className="px-xl py-md text-charcoal text-[14px] font-mono">
{(() => {
const items = [
...(sim.tactics ?? []).map((t) => t.id),
@@ -213,7 +213,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
<td className="px-xl py-md">
<SimulationStatusBadge status={sim.status} />
</td>
<td className="px-xl py-md text-charcoal text-[14px]">
<td className="px-xl py-md text-charcoal text-[14px] font-mono">
{formatDate(sim.executed_at)}
</td>
</tr>

View File

@@ -7,19 +7,17 @@ const LABELS: Record<SimulationStatus, string> = {
done: 'Done',
};
// 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',
pending: 'bg-cloud text-graphite border border-hairline',
in_progress: 'bg-primary-soft text-primary-deep',
review_required: 'bg-bloom-coral text-white',
done: 'bg-storm-deep text-white',
review_required: 'bg-warn-soft text-warn',
done: 'bg-success-soft text-success',
};
export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element {
return (
<span
className={`inline-flex items-center rounded-lg px-3 py-[6px] text-[14px] leading-[1.3] font-medium ${STYLES[status]}`}
className={`inline-flex items-center rounded-pill px-3 py-[6px] text-[14px] leading-[1.3] font-medium ${STYLES[status]}`}
data-testid="simulation-status-badge"
data-status={status}
>

View File

@@ -7,17 +7,15 @@ const LABELS: Record<EngagementStatus, string> = {
};
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-white',
planned: 'bg-warn-soft text-warn border border-warn',
active: 'bg-primary-soft text-primary-deep',
closed: 'bg-cloud text-graphite border border-hairline',
};
export function StatusBadge({ status }: { status: EngagementStatus }): JSX.Element {
return (
<span
className={`inline-flex items-center rounded-lg px-3 py-[6px] text-[14px] leading-[1.3] font-medium ${STYLES[status]}`}
className={`inline-flex items-center rounded-pill px-3 py-[6px] text-[14px] leading-[1.3] font-medium ${STYLES[status]}`}
data-testid="status-badge"
data-status={status}
>

View File

@@ -34,7 +34,7 @@ export function TemplatePickerModal({
>
<div className="modal-backdrop absolute inset-0" onClick={onClose} aria-hidden="true" />
<div className="relative card-product shadow-floating dark:shadow-floating-dark max-w-xl w-full mx-md flex flex-col gap-md max-h-[80vh] overflow-hidden">
<div className="relative card-product max-w-xl w-full mx-md flex flex-col gap-md max-h-[80vh] overflow-hidden">
<div className="flex items-center justify-between">
<h2 id="tpl-picker-title" className="text-[20px] font-medium text-ink">
From template
@@ -43,7 +43,7 @@ export function TemplatePickerModal({
type="button"
aria-label="Close"
onClick={onClose}
className="text-graphite hover:text-ink transition-colors text-[20px] leading-none"
className="text-graphite hover:text-ink text-[20px] leading-none"
>
×
</button>

View File

@@ -1,9 +1,5 @@
import { useToast } from '@/hooks/useToast';
/**
* Stack of toast notifications anchored bottom-right.
* Pure DESIGN.md surfaces: rounded-xl, soft-lift, ink slab for errors.
*/
export function ToastViewport(): JSX.Element {
const { toasts, dismiss } = useToast();
return (
@@ -15,19 +11,18 @@ 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-slab text-slab-text'
? 'bg-paper text-ink border border-hairline border-l-4 border-l-bloom-deep'
: isSuccess
? 'bg-primary text-white'
: 'bg-canvas text-ink border border-hairline';
? 'bg-paper text-ink border border-hairline border-l-4 border-l-success'
: 'bg-paper text-ink border border-hairline border-l-4 border-l-primary';
return (
<div
key={t.id}
role="status"
data-testid="toast"
data-kind={t.kind}
className={`pointer-events-auto rounded-xl px-md py-sm shadow-soft-lift text-[14px] leading-[1.4] ${surface}`}
className={`pointer-events-auto rounded-none px-md py-sm text-[14px] leading-[1.4] ${surface}`}
>
<div className="flex items-start justify-between gap-sm">
<span className="flex-1">{t.message}</span>