fix(ui): unify icon badges — solid tile + white glyph via shared helper

The labelled icon-badge tiles were inconsistent: some used a same-hue
light tile + colored glyph (e.g. the Settings 2FA row: success.light tile
+ success.main glyph — low contrast, the reported "badly visible" icon),
and the solid-tile ones used the bright .main fill in dark mode without
darkening, so a white glyph on bright green/amber was low-contrast too
(DashProfile's success badge was ~1.9:1).

Add a single `iconBadgeSx(color)` helper (theme.ts): solid `<color>.main`
fill + white glyph, darkened in dark mode (FILLED_DARK_BG) so white stays
legible; `default` = neutral grey. Applied to every badge so they can't
drift: StatCard, DashActivityFeed, DashProfile (2FA badge), Dashboard
(2FA banner icon), AttendanceHistory (month tile), and the Settings 2FA
row tile (now solid green when required / grey when optional + white lock,
instead of the low-contrast light-green tile). Also switched DashProfile's
backup-codes warning callout from warning.light to a subtle warning wash
so its amber text stays readable in dark mode.

tsc -b --noEmit, npm run build, vitest 152/152 all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 21:34:06 +02:00
parent ca092c6166
commit 54f3c414f5
7 changed files with 109 additions and 80 deletions

View File

@@ -2,6 +2,7 @@ import { Link as RouterLink } from "react-router-dom";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Card, Button, EmptyState } from "../../ui"; import { Card, Button, EmptyState } from "../../ui";
import { iconBadgeSx, type IconBadgeColor } from "../../theme";
import { import {
ENTITY_TYPE_LABELS, ENTITY_TYPE_LABELS,
getActivityIconClass, getActivityIconClass,
@@ -159,7 +160,8 @@ export default function DashActivityFeed({
}} }}
> >
<Box <Box
sx={{ sx={[
{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -167,9 +169,11 @@ export default function DashActivityFeed({
height: 32, height: 32,
borderRadius: "50%", borderRadius: "50%",
flexShrink: 0, flexShrink: 0,
bgcolor: isDefault ? "grey.600" : `${badgeColor}.main`, },
color: "common.white", iconBadgeSx(
}} isDefault ? "default" : (badgeColor as IconBadgeColor),
),
]}
> >
{getActivityIcon(act.action)} {getActivityIcon(act.action)}
</Box> </Box>

View File

@@ -10,6 +10,7 @@ import DialogActions from "@mui/material/DialogActions";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
import { useAlert } from "../../context/AlertContext"; import { useAlert } from "../../context/AlertContext";
import apiFetch from "../../utils/api"; import apiFetch from "../../utils/api";
import { iconBadgeSx } from "../../theme";
import { Card, Button, Modal, Field, TextField } from "../../ui"; import { Card, Button, Modal, Field, TextField } from "../../ui";
import useDialogScrollLock from "../../ui/useDialogScrollLock"; import useDialogScrollLock from "../../ui/useDialogScrollLock";
@@ -251,7 +252,8 @@ export default function DashProfile({
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box <Box
sx={{ sx={[
{
width: 36, width: 36,
height: 36, height: 36,
borderRadius: "50%", borderRadius: "50%",
@@ -259,9 +261,9 @@ export default function DashProfile({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexShrink: 0, flexShrink: 0,
bgcolor: totpEnabled ? "success.main" : "grey.600", },
color: "common.white", iconBadgeSx(totpEnabled ? "success" : "default"),
}} ]}
> >
<svg <svg
width="18" width="18"
@@ -425,7 +427,8 @@ export default function DashProfile({
p: 1.5, p: 1.5,
mb: 2, mb: 2,
borderRadius: 2, borderRadius: 2,
bgcolor: "warning.light", bgcolor:
"rgba(var(--mui-palette-warning-mainChannel) / 0.12)",
color: "warning.main", color: "warning.main",
fontSize: "0.875rem", fontSize: "0.875rem",
}} }}

View File

@@ -5,6 +5,7 @@ import Typography from "@mui/material/Typography";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import Forbidden from "../components/Forbidden"; import Forbidden from "../components/Forbidden";
import { companySettingsOptions } from "../lib/queries/settings"; import { companySettingsOptions } from "../lib/queries/settings";
import { iconBadgeSx } from "../theme";
import { import {
attendanceHistoryOptions, attendanceHistoryOptions,
type AttendanceRecord, type AttendanceRecord,
@@ -533,17 +534,18 @@ export default function AttendanceHistory() {
}} }}
> >
<Box <Box
sx={{ sx={[
{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
width: 44, width: 44,
height: 44, height: 44,
borderRadius: 2, borderRadius: 2,
bgcolor: "info.main",
color: "#fff",
flexShrink: 0, flexShrink: 0,
}} },
iconBadgeSx("info"),
]}
> >
<svg <svg
width="24" width="24"

View File

@@ -12,6 +12,7 @@ import { require2FAOptions } from "../lib/queries/settings";
import { getCzechDate } from "../utils/dashboardHelpers"; import { getCzechDate } from "../utils/dashboardHelpers";
import { useApiMutation } from "../lib/queries/mutations"; import { useApiMutation } from "../lib/queries/mutations";
import { Card, Button, StatusChip, PageEnter } from "../ui"; import { Card, Button, StatusChip, PageEnter } from "../ui";
import { iconBadgeSx } from "../theme";
import DashKpiCards from "../components/dashboard/DashKpiCards"; import DashKpiCards from "../components/dashboard/DashKpiCards";
import DashQuickActions from "../components/dashboard/DashQuickActions"; import DashQuickActions from "../components/dashboard/DashQuickActions";
import DashActivityFeed from "../components/dashboard/DashActivityFeed"; import DashActivityFeed from "../components/dashboard/DashActivityFeed";
@@ -253,17 +254,18 @@ export default function Dashboard() {
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box <Box
sx={{ sx={[
{
width: 40, width: 40,
height: 40, height: 40,
borderRadius: "50%", borderRadius: "50%",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
bgcolor: "error.main",
color: "#fff",
flexShrink: 0, flexShrink: 0,
}} },
iconBadgeSx("error"),
]}
> >
<svg <svg
width="20" width="20"

View File

@@ -33,6 +33,7 @@ import {
import { useApiMutation } from "../lib/queries/mutations"; import { useApiMutation } from "../lib/queries/mutations";
import apiFetch from "../utils/api"; import apiFetch from "../utils/api";
import { iconBadgeSx } from "../theme";
const API_BASE = "/api/admin"; const API_BASE = "/api/admin";
const PlusIcon = ( const PlusIcon = (
@@ -842,17 +843,18 @@ export default function Settings() {
> >
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box <Box
sx={{ sx={[
{
width: 36, width: 36,
height: 36, height: 36,
borderRadius: "50%", borderRadius: "50%",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
bgcolor: require2FA ? "success.light" : "action.hover",
color: require2FA ? "success.main" : "text.secondary",
flexShrink: 0, flexShrink: 0,
}} },
iconBadgeSx(require2FA ? "success" : "default"),
]}
> >
<svg <svg
width="18" width="18"

View File

@@ -1,4 +1,4 @@
import { createTheme } from "@mui/material/styles"; import { createTheme, type Theme } from "@mui/material/styles";
const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif"; const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif";
const FONT_HEADING = "'Urbanist', sans-serif"; const FONT_HEADING = "'Urbanist', sans-serif";
@@ -226,3 +226,29 @@ export const fonts = {
heading: FONT_HEADING, heading: FONT_HEADING,
mono: FONT_MONO, mono: FONT_MONO,
}; };
export type IconBadgeColor =
| "default"
| "primary"
| "success"
| "warning"
| "error"
| "info";
/**
* sx fragment for a solid icon-badge tile: a `<color>.main` fill with a WHITE
* glyph, darkened in dark mode (FILLED_DARK_BG) so white stays legible on the
* otherwise-bright success/warning/info/error fills. `default` = neutral grey.
* Single source of truth for every labelled icon badge — use in an sx array
* next to the tile's size/shape so they can never drift apart:
* sx={[{ width: 40, height: 40, borderRadius: 2, display: "flex", … }, iconBadgeSx(color)]}
*/
export function iconBadgeSx(color: IconBadgeColor) {
const bg = color === "default" ? "grey.600" : `${color}.main`;
const darkBg = (FILLED_DARK_BG as Record<string, { bg: string }>)[color]?.bg;
return (theme: Theme) => ({
bgcolor: bg,
color: "common.white",
...(darkBg ? theme.applyStyles("dark", { backgroundColor: darkBg }) : {}),
});
}

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Card from "./Card"; import Card from "./Card";
import { FILLED_DARK_BG } from "../theme"; import { iconBadgeSx } from "../theme";
export type StatCardColor = export type StatCardColor =
| "default" | "default"
@@ -31,33 +31,23 @@ export default function StatCard({
color = "default", color = "default",
footer, footer,
}: StatCardProps) { }: StatCardProps) {
// Solid tile + white glyph: high-contrast in both themes (the old light tint
// left the icon dark/low-visibility, esp. for the default variant). In dark
// mode the bright .main fill is darkened (same rule as filled buttons/chips)
// so the white glyph stays legible on success/warning/info/error tiles.
const iconBg = color === "default" ? "grey.600" : `${color}.main`;
const iconDarkBg = (FILLED_DARK_BG as Record<string, { bg: string }>)[color]
?.bg;
return ( return (
<Card> <Card>
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5 }}> <Box sx={{ display: "flex", alignItems: "flex-start", gap: 1.5 }}>
{icon && ( {icon && (
<Box <Box
sx={(theme) => ({ sx={[
{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 2, borderRadius: 2,
bgcolor: iconBg,
color: "common.white",
flexShrink: 0, flexShrink: 0,
...(iconDarkBg },
? theme.applyStyles("dark", { backgroundColor: iconDarkBg }) iconBadgeSx(color),
: {}), ]}
})}
> >
{icon} {icon}
</Box> </Box>