refactor(components): squared shapes + mono data cells across all components
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user