Skip to content

Commit bad7d8c

Browse files
rdmuellerclaude
andcommitted
refactor: extract MitigationCard into separate component with CSS module
Move MitigationCard to src/components/MitigationCard.jsx. Static layout styles moved to MitigationCard.module.css, dynamic color styles stay inline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 213c98f commit bad7d8c

3 files changed

Lines changed: 138 additions & 36 deletions

File tree

src/RiskRadar.jsx

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,7 @@ import { useTheme } from "./theme.js";
55
import { VERSION, TIER_BG, TYPE_COLORS } from "./constants.js";
66
import { getTierIndex, detectBrowserLanguage } from "./utils.js";
77
import RadarChart from "./components/RadarChart.jsx";
8-
9-
function MitigationCard({ group, active, accent, t }) {
10-
const [open, setOpen] = useState(false);
11-
return (
12-
<div style={{ border: `2px solid ${active ? accent : "var(--border-subtle)"}`, borderRadius: 12, background: active ? `${accent}10` : "var(--bg-main)", padding: "12px 14px", opacity: active ? 1 : 0.5, transition: "all 0.3s" }}>
13-
<div onClick={() => active && setOpen(!open)} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: active ? "pointer" : "default" }}>
14-
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
15-
<span style={{ fontSize: 24 }}>{group.icon}</span>
16-
<div>
17-
<div style={{ fontWeight: 700, fontSize: 18, color: active ? "var(--text-heading)" : "var(--text-secondary)" }}>{group.title}</div>
18-
<div style={{ fontSize: 13, color: "var(--text-secondary)" }}>{group.measures.length} {group.measures.length !== 1 ? t.measures : t.measure}</div>
19-
</div>
20-
</div>
21-
{active && <span style={{ fontSize: 22, color: "var(--text-secondary)", transform: open ? "rotate(180deg)" : "rotate(0)", transition: "transform 0.2s" }}></span>}
22-
</div>
23-
{active && open && (
24-
<div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 6 }}>
25-
{group.measures.map((m, i) => {
26-
const tc = TYPE_COLORS[m.type];
27-
return (
28-
<div key={i} style={{ background: "var(--bg-card)", borderRadius: 8, padding: "8px 10px", borderLeft: `3px solid ${tc.color}` }}>
29-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
30-
<span style={{ fontWeight: 600, fontSize: 16, color: "var(--text-primary)" }}>{m.name}</span>
31-
<span style={{ fontSize: 11, fontWeight: 700, color: tc.color, background: tc.bg, padding: "2px 6px", borderRadius: 3, textTransform: "uppercase", letterSpacing: "0.05em" }}>
32-
{t.typeBadges[m.type]}
33-
</span>
34-
</div>
35-
<div style={{ fontSize: 15, color: "var(--text-secondary)", marginTop: 3, lineHeight: 1.4 }}>{m.desc}</div>
36-
</div>
37-
);
38-
})}
39-
</div>
40-
)}
41-
</div>
42-
);
43-
}
8+
import MitigationCard from "./components/MitigationCard.jsx";
449

4510
const adoc = Asciidoctor();
4611

src/components/MitigationCard.jsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useState } from "react";
2+
import { TYPE_COLORS } from "../constants.js";
3+
import styles from "./MitigationCard.module.css";
4+
5+
export default function MitigationCard({ group, active, accent, t }) {
6+
const [open, setOpen] = useState(false);
7+
return (
8+
<div
9+
className={`${styles.card} ${active ? "" : styles.cardInactive}`}
10+
style={{
11+
border: `2px solid ${active ? accent : "var(--border-subtle)"}`,
12+
background: active ? `${accent}10` : "var(--bg-main)",
13+
}}
14+
>
15+
<div
16+
onClick={() => active && setOpen(!open)}
17+
className={styles.header}
18+
style={{ cursor: active ? "pointer" : "default" }}
19+
>
20+
<div className={styles.headerInfo}>
21+
<span className={styles.icon}>{group.icon}</span>
22+
<div>
23+
<div className={styles.title} style={{ color: active ? "var(--text-heading)" : "var(--text-secondary)" }}>{group.title}</div>
24+
<div className={styles.subtitle}>{group.measures.length} {group.measures.length !== 1 ? t.measures : t.measure}</div>
25+
</div>
26+
</div>
27+
{active && <span className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`}></span>}
28+
</div>
29+
{active && open && (
30+
<div className={styles.measures}>
31+
{group.measures.map((m, i) => {
32+
const tc = TYPE_COLORS[m.type];
33+
return (
34+
<div key={i} className={styles.measure} style={{ borderLeft: `3px solid ${tc.color}` }}>
35+
<div className={styles.measureHeader}>
36+
<span className={styles.measureName}>{m.name}</span>
37+
<span className={styles.typeBadge} style={{ color: tc.color, background: tc.bg }}>
38+
{t.typeBadges[m.type]}
39+
</span>
40+
</div>
41+
<div className={styles.measureDesc}>{m.desc}</div>
42+
</div>
43+
);
44+
})}
45+
</div>
46+
)}
47+
</div>
48+
);
49+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
.card {
2+
border-radius: 12px;
3+
padding: 12px 14px;
4+
transition: all 0.3s;
5+
}
6+
7+
.cardInactive {
8+
opacity: 0.5;
9+
}
10+
11+
.header {
12+
display: flex;
13+
justify-content: space-between;
14+
align-items: center;
15+
}
16+
17+
.headerInfo {
18+
display: flex;
19+
align-items: center;
20+
gap: 8px;
21+
}
22+
23+
.icon {
24+
font-size: 24px;
25+
}
26+
27+
.title {
28+
font-weight: 700;
29+
font-size: 18px;
30+
}
31+
32+
.subtitle {
33+
font-size: 13px;
34+
color: var(--text-secondary);
35+
}
36+
37+
.chevron {
38+
font-size: 22px;
39+
color: var(--text-secondary);
40+
transition: transform 0.2s;
41+
}
42+
43+
.chevronOpen {
44+
transform: rotate(180deg);
45+
}
46+
47+
.measures {
48+
margin-top: 10px;
49+
display: flex;
50+
flex-direction: column;
51+
gap: 6px;
52+
}
53+
54+
.measure {
55+
background: var(--bg-card);
56+
border-radius: 8px;
57+
padding: 8px 10px;
58+
}
59+
60+
.measureHeader {
61+
display: flex;
62+
justify-content: space-between;
63+
align-items: center;
64+
gap: 6px;
65+
flex-wrap: wrap;
66+
}
67+
68+
.measureName {
69+
font-weight: 600;
70+
font-size: 16px;
71+
color: var(--text-primary);
72+
}
73+
74+
.typeBadge {
75+
font-size: 11px;
76+
font-weight: 700;
77+
padding: 2px 6px;
78+
border-radius: 3px;
79+
text-transform: uppercase;
80+
letter-spacing: 0.05em;
81+
}
82+
83+
.measureDesc {
84+
font-size: 15px;
85+
color: var(--text-secondary);
86+
margin-top: 3px;
87+
line-height: 1.4;
88+
}

0 commit comments

Comments
 (0)