|
1 | | -import React from "react"; |
2 | | -import { Box } from "@mui/material"; |
3 | | -import { IChipProps, ChipVariant, ChipColorConfig } from "../types/interfaces/i.chip"; |
4 | | -import { background, status, risk } from "../themes/palette"; |
5 | | -import { fontSize, fontWeight } from "../themes/typography"; |
6 | | - |
7 | 1 | /** |
8 | | - * Color mappings for all chip variants |
9 | | - * Light pastel backgrounds with dark matching text |
| 2 | + * Compatibility export. New screens should import StatusBadge. |
| 3 | + * Existing call sites keep `import Chip from ".../Chip"`. |
10 | 4 | */ |
11 | | -const VARIANT_COLORS: Record<ChipVariant, ChipColorConfig> = { |
12 | | - // Risk levels |
13 | | - "critical": { backgroundColor: `${risk.critical.bg}`, textColor: `${status.error.text}` }, |
14 | | - "high": { backgroundColor: `${risk.high.bg}`, textColor: `${risk.high.text}` }, |
15 | | - "medium": { backgroundColor: `${risk.medium.bg}`, textColor: `${risk.medium.text}` }, |
16 | | - "low": { backgroundColor: `${risk.low.bg}`, textColor: `${status.success.text}` }, |
17 | | - "very-low": { backgroundColor: `${risk.veryLow.bg}`, textColor: `${risk.veryLow.text}` }, |
18 | | - |
19 | | - // Status |
20 | | - "success": { backgroundColor: `${status.success.bg}`, textColor: `${status.success.text}` }, |
21 | | - "warning": { backgroundColor: `${status.warning.bg}`, textColor: `${status.warning.text}` }, |
22 | | - "error": { backgroundColor: `${status.error.bg}`, textColor: `${status.error.text}` }, |
23 | | - "info": { backgroundColor: `${status.info.bg}`, textColor: `${status.info.text}` }, |
24 | | - "default": { backgroundColor: `${background.hover}`, textColor: `${status.default.text}` }, |
25 | | - |
26 | | - // Severity (maps to same colors as risk levels) |
27 | | - "catastrophic": { backgroundColor: `${risk.critical.bg}`, textColor: `${status.error.text}` }, |
28 | | - "major": { backgroundColor: `${risk.high.bg}`, textColor: `${risk.high.text}` }, |
29 | | - "moderate": { backgroundColor: `${risk.medium.bg}`, textColor: `${risk.medium.text}` }, |
30 | | - "minor": { backgroundColor: `${risk.low.bg}`, textColor: `${status.success.text}` }, |
31 | | - "negligible": { backgroundColor: `${risk.veryLow.bg}`, textColor: `${risk.veryLow.text}` }, |
32 | | - |
33 | | - // Boolean |
34 | | - "yes": { backgroundColor: `${status.success.bg}`, textColor: `${status.success.text}` }, |
35 | | - "no": { backgroundColor: `${status.error.bg}`, textColor: `${status.error.text}` }, |
36 | | -}; |
37 | | - |
38 | | -/** |
39 | | - * Height values for chip sizes |
40 | | - */ |
41 | | -const SIZE_HEIGHT: Record<"small" | "medium", number> = { |
42 | | - small: 24, |
43 | | - medium: 34, |
44 | | -}; |
45 | | - |
46 | | -/** |
47 | | - * Map common label strings to variants (case-insensitive) |
48 | | - */ |
49 | | -const LABEL_TO_VARIANT: Record<string, ChipVariant> = { |
50 | | - // Risk levels |
51 | | - "critical": "critical", |
52 | | - "very high": "critical", |
53 | | - "very high risk": "critical", |
54 | | - "high": "high", |
55 | | - "high risk": "high", |
56 | | - "medium": "medium", |
57 | | - "medium risk": "medium", |
58 | | - "low": "low", |
59 | | - "low risk": "low", |
60 | | - "very low": "very-low", |
61 | | - "very low risk": "very-low", |
62 | | - |
63 | | - // Severity |
64 | | - "catastrophic": "catastrophic", |
65 | | - "major": "major", |
66 | | - "moderate": "moderate", |
67 | | - "minor": "minor", |
68 | | - "negligible": "negligible", |
69 | | - |
70 | | - // ISO clause/annex statuses |
71 | | - "awaiting review": "info", |
72 | | - "awaiting approval": "info", |
73 | | - "implemented": "success", |
74 | | - "done": "success", |
75 | | - "audited": "success", |
76 | | - "needs rework": "warning", |
77 | | - |
78 | | - // Status |
79 | | - "approved": "success", |
80 | | - "completed": "success", |
81 | | - "confirmed": "success", |
82 | | - "resolved": "success", |
83 | | - "active": "success", |
84 | | - "yes": "yes", |
85 | | - "pending": "warning", |
86 | | - "pending review": "warning", |
87 | | - "unreviewed": "warning", |
88 | | - "in progress": "warning", |
89 | | - "under review": "warning", |
90 | | - "draft": "default", |
91 | | - "superseded": "default", |
92 | | - "resubmitted": "info", |
93 | | - "expired": "warning", |
94 | | - "blocked": "error", |
95 | | - "rejected": "error", |
96 | | - "restricted": "error", |
97 | | - "retired": "default", |
98 | | - "no": "no", |
99 | | - "open": "error", |
100 | | - "closed": "info", |
101 | | - |
102 | | - // Lifecycle stages (MLFlow) |
103 | | - "production": "success", |
104 | | - "staging": "warning", |
105 | | - "archived": "default", |
106 | | - "none": "default", |
107 | | - |
108 | | - // Incident severity |
109 | | - "serious": "high", |
110 | | - "very serious": "critical", |
111 | | - |
112 | | - // Incident status |
113 | | - "investigated": "warning", |
114 | | - "mitigated": "success", |
115 | | - |
116 | | - // Incident approval |
117 | | - "not required": "default", |
118 | | - |
119 | | - // Mitigation status |
120 | | - "not started": "default", |
121 | | - "on hold": "warning", |
122 | | - "deferred": "warning", |
123 | | - "canceled": "error", |
124 | | - "requires review": "info", |
125 | | - |
126 | | - // Likelihood |
127 | | - "almost certain": "critical", |
128 | | - "likely": "high", |
129 | | - "possible": "warning", |
130 | | - "unlikely": "low", |
131 | | - "rare": "very-low", |
132 | | - |
133 | | - // Policy status |
134 | | - "published": "info", |
135 | | - "deprecated": "error", |
136 | | - |
137 | | - // Training status |
138 | | - "planned": "info", |
139 | | - |
140 | | - // Shadow AI statuses |
141 | | - "detected": "info", |
142 | | - "governed": "success", |
143 | | - "inactive": "default", |
144 | | - "revoked": "error", |
145 | | - "dismissed": "default", |
146 | | - |
147 | | - // AI risk classification (EU AI Act) |
148 | | - "prohibited": "critical", |
149 | | - "limited risk": "warning", |
150 | | - "minimal risk": "success", |
151 | | - |
152 | | - // Automation execution status |
153 | | - "partial success": "warning", |
154 | | - "failure": "error", |
155 | | - |
156 | | - // Guardrail actions |
157 | | - "block": "error", |
158 | | - "mask": "warning", |
159 | | -}; |
160 | | - |
161 | | -/** |
162 | | - * Get variant from label if no explicit variant provided |
163 | | - */ |
164 | | -const getVariantFromLabel = (label: string): ChipVariant | undefined => { |
165 | | - // The prop is typed string, but nullable columns still reach callers as null |
166 | | - // at runtime. Deriving a variant is a convenience, so give up quietly rather |
167 | | - // than throw — a throw here escapes to the error boundary and blanks the page. |
168 | | - if (typeof label !== "string") return undefined; |
169 | | - const normalizedLabel = label.toLowerCase().trim(); |
170 | | - return LABEL_TO_VARIANT[normalizedLabel]; |
171 | | -}; |
172 | | - |
173 | | -/** |
174 | | - * Darken a hex color by a percentage |
175 | | - */ |
176 | | -const darkenColor = (hex: string, percent: number): string => { |
177 | | - // Remove # if present |
178 | | - const color = hex.replace("#", ""); |
179 | | - |
180 | | - // Parse RGB values |
181 | | - const r = parseInt(color.substring(0, 2), 16); |
182 | | - const g = parseInt(color.substring(2, 4), 16); |
183 | | - const b = parseInt(color.substring(4, 6), 16); |
184 | | - |
185 | | - // Darken each channel |
186 | | - const darkenChannel = (channel: number) => Math.max(0, Math.floor(channel * (1 - percent / 100))); |
187 | | - |
188 | | - const newR = darkenChannel(r); |
189 | | - const newG = darkenChannel(g); |
190 | | - const newB = darkenChannel(b); |
191 | | - |
192 | | - // Convert back to hex |
193 | | - return `#${newR.toString(16).padStart(2, "0")}${newG.toString(16).padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`; |
194 | | -}; |
195 | | - |
196 | | -/** |
197 | | - * Get colors for a chip based on variant or custom colors |
198 | | - */ |
199 | | -const getChipColors = ( |
200 | | - label: string, |
201 | | - variant?: ChipVariant, |
202 | | - backgroundColor?: string, |
203 | | - textColor?: string, |
204 | | -): ChipColorConfig => { |
205 | | - // Custom colors take priority |
206 | | - if (backgroundColor && textColor) { |
207 | | - return { backgroundColor, textColor }; |
208 | | - } |
209 | | - |
210 | | - // Use explicit variant if provided |
211 | | - if (variant && VARIANT_COLORS[variant]) { |
212 | | - return { |
213 | | - backgroundColor: backgroundColor || VARIANT_COLORS[variant].backgroundColor, |
214 | | - textColor: textColor || VARIANT_COLORS[variant].textColor, |
215 | | - }; |
216 | | - } |
217 | | - |
218 | | - // Try to derive variant from label |
219 | | - const derivedVariant = getVariantFromLabel(label); |
220 | | - if (derivedVariant && VARIANT_COLORS[derivedVariant]) { |
221 | | - return { |
222 | | - backgroundColor: backgroundColor || VARIANT_COLORS[derivedVariant].backgroundColor, |
223 | | - textColor: textColor || VARIANT_COLORS[derivedVariant].textColor, |
224 | | - }; |
225 | | - } |
226 | | - |
227 | | - // Fallback to default |
228 | | - return VARIANT_COLORS.default; |
229 | | -}; |
230 | | - |
231 | | -/** |
232 | | - * Unified Chip component with consistent light pastel styling |
233 | | - * |
234 | | - * @example |
235 | | - * // Risk level |
236 | | - * <Chip label="High" variant="high" /> |
237 | | - * |
238 | | - * @example |
239 | | - * // Status |
240 | | - * <Chip label="Approved" variant="success" /> |
241 | | - * |
242 | | - * @example |
243 | | - * // Boolean |
244 | | - * <Chip label="Yes" variant="yes" /> |
245 | | - * |
246 | | - * @example |
247 | | - * // Custom colors |
248 | | - * <Chip label="Custom" backgroundColor="#E8F5E9" textColor="#2E7D32" /> |
249 | | - * |
250 | | - * @example |
251 | | - * // Lowercase, medium size |
252 | | - * <Chip label="In Progress" variant="warning" uppercase={false} size="medium" /> |
253 | | - */ |
254 | | -const Chip: React.FC<IChipProps> = ({ |
255 | | - label, |
256 | | - variant, |
257 | | - size = "small", |
258 | | - uppercase = true, |
259 | | - backgroundColor, |
260 | | - textColor, |
261 | | - icon, |
262 | | -}) => { |
263 | | - const colors = getChipColors(label, variant, backgroundColor, textColor); |
264 | | - const height = SIZE_HEIGHT[size]; |
265 | | - |
266 | | - // Create gradient colors (very slightly darker at bottom) |
267 | | - const gradientTop = colors.backgroundColor; |
268 | | - const gradientBottom = darkenColor(colors.backgroundColor, 3); |
269 | | - |
270 | | - // Create border color (very slightly darker than background) |
271 | | - const borderColor = darkenColor(colors.backgroundColor, 6); |
272 | | - |
273 | | - return ( |
274 | | - <Box |
275 | | - component="span" |
276 | | - sx={{ |
277 | | - display: "inline-flex", |
278 | | - alignItems: "center", |
279 | | - justifyContent: "center", |
280 | | - height, |
281 | | - py: 2, |
282 | | - px: 4, |
283 | | - borderRadius: "4px", |
284 | | - background: `linear-gradient(180deg, ${gradientTop} 0%, ${gradientBottom} 100%)`, |
285 | | - border: `1px solid ${borderColor}`, |
286 | | - color: colors.textColor, |
287 | | - fontSize: fontSize.caption, |
288 | | - fontWeight: fontWeight.regular, |
289 | | - textTransform: uppercase ? "uppercase" : "none", |
290 | | - whiteSpace: "nowrap", |
291 | | - lineHeight: 1, |
292 | | - }} |
293 | | - > |
294 | | - {icon && ( |
295 | | - <Box component="span" sx={{ display: "inline-flex", alignItems: "center", mr: 1 }}> |
296 | | - {icon} |
297 | | - </Box> |
298 | | - )} |
299 | | - {label} |
300 | | - </Box> |
301 | | - ); |
302 | | -}; |
303 | | - |
304 | | -export default Chip; |
305 | | - |
306 | | -/** |
307 | | - * Export variant colors for external use if needed |
308 | | - */ |
309 | | -export { VARIANT_COLORS, getChipColors }; |
| 5 | +export { default, VARIANT_COLORS, getChipColors } from "./StatusBadge"; |
0 commit comments