refactor(components): squared shapes + mono data cells across all components

This commit is contained in:
Knacky
2026-06-09 18:42:26 +02:00
parent c791e50108
commit ec7800ae38
15 changed files with 42 additions and 51 deletions

View File

@@ -25,7 +25,7 @@ export function ConfirmDialog({
className="fixed inset-0 z-50 flex items-center justify-center" 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="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"> <h2 id="confirm-dialog-title" className="text-[20px] font-medium text-ink">
{title} {title}
</h2> </h2>

View File

@@ -10,7 +10,7 @@ export function EmptyState({ title, description, action }: EmptyStateProps): JSX
return ( return (
<div <div
data-testid="empty-state" 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> <h2 className="text-[24px] font-medium text-ink">{title}</h2>
{description ? <p className="text-[16px] text-charcoal">{description}</p> : null} {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 <div
role="alert" role="alert"
data-testid="error-state" 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> <h2 className="text-[24px] font-medium text-bloom-deep">{title}</h2>
<p className="text-[16px] text-charcoal">{message}</p> <p className="text-[16px] text-charcoal">{message}</p>

View File

@@ -54,7 +54,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
<div className="inline-flex"> <div className="inline-flex">
<button <button
type="button" type="button"
className="btn-outline rounded-r-none border-r-0" className="btn-outline border-r-0"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
data-testid="export-btn" data-testid="export-btn"
> >
@@ -64,7 +64,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
type="button" type="button"
aria-label="Export options" aria-label="Export options"
aria-expanded={open} aria-expanded={open}
className="btn-outline rounded-l-none px-sm" className="btn-outline px-sm"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
data-testid="export-dropdown-toggle" data-testid="export-dropdown-toggle"
> >
@@ -74,7 +74,7 @@ export function ExportEngagementButton({ engagementId }: ExportEngagementButtonP
{open ? ( {open ? (
<div <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" role="menu"
> >
{FORMATS.map(({ label, value }) => ( {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> <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-slab-muted"> <span className="text-[12px] uppercase tracking-[0.5px] text-slab-muted font-mono">
{user.role} {user.role}
</span> </span>
<span className="text-[14px]">{user.username}</span> <span className="text-[14px] font-mono">{user.username}</span>
<button <button
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-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} /> <ThemeIcon theme={theme} />
<span className="uppercase tracking-[0.5px]">{themeLabel(theme)}</span> <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" role="status"
aria-live="polite" aria-live="polite"
data-testid="loading-state" 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} {label}
</div> </div>
); );

View File

@@ -170,7 +170,7 @@ export function MitreMatrixModal({
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 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' }} style={{ width: '1400px' }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
@@ -230,7 +230,7 @@ export function MitreMatrixModal({
type="button" type="button"
onClick={() => toggleTactic(tactic.tactic_id)} onClick={() => toggleTactic(tactic.tactic_id)}
title={`${tactic.tactic_name} (${tactic.tactic_id}) — click to tag this tactic`} 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 tacticSelected
? 'bg-primary border-primary' ? 'bg-primary border-primary'
: 'bg-cloud hover:bg-fog' : 'bg-cloud hover:bg-fog'
@@ -251,7 +251,7 @@ export function MitreMatrixModal({
</button> </button>
{/* Techniques */} {/* 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) => { {visibleTechniques.map((tech, techIdx) => {
const isSelected = selectedTechMap.has(tech.id); const isSelected = selectedTechMap.has(tech.id);
const isExpanded = expandedTechniques.has(tech.id) || autoExpanded.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' 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'}`}> <span className={`block truncate text-[10px] ${isSelected ? 'text-white/80' : 'text-charcoal'}`}>
{tech.name} {tech.name}
</span> </span>
@@ -318,7 +318,7 @@ export function MitreMatrixModal({
: 'bg-cloud text-charcoal hover:bg-fog' : '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> <span className="block truncate">{sub.name}</span>
</button> </button>
); );

View File

@@ -107,7 +107,7 @@ export function MitreTechniquePicker({
/> />
{open && ( {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 && ( {isFetching && (
<div className="px-md py-sm text-[14px] text-graphite">Searching</div> <div className="px-md py-sm text-[14px] text-graphite">Searching</div>
)} )}
@@ -144,10 +144,10 @@ export function MitreTechniquePicker({
selectItem(item); 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> <span className="text-charcoal"> {item.name}</span>
{item.tactics.length > 0 && ( {item.tactics.length > 0 && (
<span className="text-graphite"> ({item.tactics[0]})</span> <span className="font-mono text-graphite"> ({item.tactics[0]})</span>
)} )}
</li> </li>
))} ))}

View File

@@ -12,7 +12,7 @@ interface TacticTagProps {
disabled?: boolean; disabled?: boolean;
} }
// Technique chip — soft blue, id only, name in title // Technique tag — angular, soft blue, monospace ID
export function MitreTechniqueTag({ export function MitreTechniqueTag({
technique, technique,
onRemove, onRemove,
@@ -22,7 +22,7 @@ export function MitreTechniqueTag({
<span <span
data-testid="mitre-technique-tag" data-testid="mitre-technique-tag"
title={`${technique.id}${technique.name}`} 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} {technique.id}
{!disabled && ( {!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({ export function MitreTacticTag({
tactic, tactic,
onRemove, onRemove,
@@ -49,7 +49,7 @@ export function MitreTacticTag({
<span <span
data-testid="mitre-tactic-tag" data-testid="mitre-tactic-tag"
title={`${tactic.id}${tactic.name}`} 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} {tactic.id}
{!disabled && ( {!disabled && (

View File

@@ -116,7 +116,7 @@ export function MitreTechniquesField({
aria-label="Open MITRE matrix" aria-label="Open MITRE matrix"
onClick={() => { setShowPicker(false); setShowMatrix(true); }} onClick={() => { setShowPicker(false); setShowMatrix(true); }}
disabled={isPending} 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} /> <Grid2x2 size={16} />
</button> </button>

View File

@@ -73,7 +73,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
<div className="inline-flex"> <div className="inline-flex">
<button <button
type="button" type="button"
className="btn-primary rounded-r-none border-r border-primary-deep" className="btn-primary border-r border-primary-deep"
onClick={handleBlank} onClick={handleBlank}
data-testid="new-simulation-btn" data-testid="new-simulation-btn"
> >
@@ -83,7 +83,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
type="button" type="button"
aria-label="More options" aria-label="More options"
aria-expanded={open} aria-expanded={open}
className="btn-primary rounded-l-none px-sm" className="btn-primary px-sm"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
data-testid="new-simulation-dropdown-toggle" data-testid="new-simulation-dropdown-toggle"
> >
@@ -93,7 +93,7 @@ function NewSimulationDropdown({ engagementId }: { engagementId: number }): JSX.
{open ? ( {open ? (
<div <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" role="menu"
> >
<button <button
@@ -199,7 +199,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
{sim.name} {sim.name}
</Link> </Link>
</td> </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 = [ const items = [
...(sim.tactics ?? []).map((t) => t.id), ...(sim.tactics ?? []).map((t) => t.id),
@@ -213,7 +213,7 @@ export function SimulationList({ engagementId }: SimulationListProps): JSX.Eleme
<td className="px-xl py-md"> <td className="px-xl py-md">
<SimulationStatusBadge status={sim.status} /> <SimulationStatusBadge status={sim.status} />
</td> </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)} {formatDate(sim.executed_at)}
</td> </td>
</tr> </tr>

View File

@@ -7,19 +7,17 @@ const LABELS: Record<SimulationStatus, string> = {
done: 'Done', 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> = { 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', in_progress: 'bg-primary-soft text-primary-deep',
review_required: 'bg-bloom-coral text-white', review_required: 'bg-warn-soft text-warn',
done: 'bg-storm-deep text-white', done: 'bg-success-soft text-success',
}; };
export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element { export function SimulationStatusBadge({ status }: { status: SimulationStatus }): JSX.Element {
return ( return (
<span <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-testid="simulation-status-badge"
data-status={status} data-status={status}
> >

View File

@@ -7,17 +7,15 @@ const LABELS: Record<EngagementStatus, string> = {
}; };
const STYLES: Record<EngagementStatus, string> = { const STYLES: Record<EngagementStatus, string> = {
// Outlined ink for planned (neutral), filled primary for active (engagement live), planned: 'bg-warn-soft text-warn border border-warn',
// outlined steel for closed (muted). Stays within DESIGN.md palette. active: 'bg-primary-soft text-primary-deep',
planned: 'bg-canvas text-ink border border-ink',
active: 'bg-primary text-white',
closed: 'bg-cloud text-graphite border border-hairline', closed: 'bg-cloud text-graphite border border-hairline',
}; };
export function StatusBadge({ status }: { status: EngagementStatus }): JSX.Element { export function StatusBadge({ status }: { status: EngagementStatus }): JSX.Element {
return ( return (
<span <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-testid="status-badge"
data-status={status} 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="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"> <div className="flex items-center justify-between">
<h2 id="tpl-picker-title" className="text-[20px] font-medium text-ink"> <h2 id="tpl-picker-title" className="text-[20px] font-medium text-ink">
From template From template
@@ -43,7 +43,7 @@ export function TemplatePickerModal({
type="button" type="button"
aria-label="Close" aria-label="Close"
onClick={onClose} 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> </button>

View File

@@ -1,9 +1,5 @@
import { useToast } from '@/hooks/useToast'; 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 { export function ToastViewport(): JSX.Element {
const { toasts, dismiss } = useToast(); const { toasts, dismiss } = useToast();
return ( return (
@@ -15,19 +11,18 @@ 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-slab text-slab-text' ? 'bg-paper text-ink border border-hairline border-l-4 border-l-bloom-deep'
: isSuccess : isSuccess
? 'bg-primary text-white' ? 'bg-paper text-ink border border-hairline border-l-4 border-l-success'
: 'bg-canvas text-ink border border-hairline'; : 'bg-paper text-ink border border-hairline border-l-4 border-l-primary';
return ( return (
<div <div
key={t.id} key={t.id}
role="status" role="status"
data-testid="toast" data-testid="toast"
data-kind={t.kind} 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"> <div className="flex items-start justify-between gap-sm">
<span className="flex-1">{t.message}</span> <span className="flex-1">{t.message}</span>