feat(design): terminal-SOC aesthetic refresh (sprint 7) #10
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user