Skip to content

Commit 6fd3675

Browse files
Innaglcursoragent
andcommitted
feat(ui): add shared StatusBadge and re-export Chip
Move chip color, size, and uppercase behavior into StatusBadge so the design system has one badge. Existing screens keep importing Chip. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d0ce6df commit 6fd3675

9 files changed

Lines changed: 494 additions & 365 deletions

File tree

Lines changed: 3 additions & 307 deletions
Original file line numberDiff line numberDiff line change
@@ -1,309 +1,5 @@
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-
71
/**
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"`.
104
*/
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";

‎Clients/src/presentation/components/Chip/__tests__/Chip.test.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { screen } from "@testing-library/react";
22
import { renderWithProviders } from "../../../../test/renderWithProviders";
33
import Chip from "../../Chip";
4+
import StatusBadge from "../../StatusBadge";
45

56
describe("Chip", () => {
7+
it("re-exports StatusBadge", () => {
8+
expect(Chip).toBe(StatusBadge);
9+
});
10+
611
it("renders the label text", () => {
712
renderWithProviders(<Chip label="Active" />);
813
expect(screen.getByText("Active")).toBeInTheDocument();

0 commit comments

Comments
 (0)