fix(ui): unify colored-control text — white on all filled, both themes

The text color on filled colored controls was inconsistent: contained
primary resolved to white in both schemes, but error/success/warning/info
used a per-scheme contrastText (#fff light, #1a1a1a dark). So in DARK mode
a primary (red) button showed white text while an error (red) button —
e.g. every ConfirmDialog delete confirm, the contained "Smazat", and the
dashboard quick-action tiles — showed near-black text. "Black text on one
red button, white on another."

Unify on a single rule: every FILLED colored surface gets WHITE text/glyph
in BOTH themes. The palette .main stays bright in dark mode (it drives text,
outlined buttons and row tints), but a bright fill makes white illegible, so
filled surfaces drop to a darker fill in dark mode (new FILLED_DARK_BG, ≈ the
light-scheme shades; all ≥4.5:1 with white):
- theme MuiButton: contained error/success/warning/info -> white text +
  darker dark-mode fill (disabled state preserved).
- theme MuiChip: filled error/success/warning/info -> white label + darker
  dark-mode fill (covers StatusChip + status pills).
- StatCard icon badges: white glyph + darker dark-mode tile.

Plus two consistency cleanups:
- Detail-page "Smazat" unified to variant=outlined color=error (Offer +
  Invoice now match Order/Project/Warehouse; the contained->confirm pattern
  stays: subtle trigger, strong contained confirm dialog).
- "Zrušit filtry" reset buttons -> color=inherit (neutral), matching the
  rest of the secondary-button family (were default outlined-primary).

Neutral (color=inherit) text/outlined buttons were already correct (label =
ambient text color) and are untouched. tsc -b --noEmit, npm run build and
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 20:41:50 +02:00
parent c7f9d9aa36
commit ca092c6166
8 changed files with 133 additions and 12 deletions

View File

@@ -1140,7 +1140,11 @@ export default function InvoiceDetail() {
</Button> </Button>
)} )}
{hasPermission("invoices.delete") && ( {hasPermission("invoices.delete") && (
<Button onClick={() => setDeleteConfirm(true)} color="error"> <Button
onClick={() => setDeleteConfirm(true)}
variant="outlined"
color="error"
>
Smazat Smazat
</Button> </Button>
)} )}
@@ -1710,7 +1714,11 @@ export default function InvoiceDetail() {
</Button> </Button>
))} ))}
{hasPermission("invoices.delete") && ( {hasPermission("invoices.delete") && (
<Button onClick={() => setDeleteConfirm(true)} color="error"> <Button
onClick={() => setDeleteConfirm(true)}
variant="outlined"
color="error"
>
Smazat Smazat
</Button> </Button>
)} )}

View File

@@ -1184,7 +1184,11 @@ export default function OfferDetail() {
</Button> </Button>
)} )}
{isEdit && hasPermission("offers.delete") && ( {isEdit && hasPermission("offers.delete") && (
<Button onClick={() => setDeleteConfirm(true)} color="error"> <Button
onClick={() => setDeleteConfirm(true)}
variant="outlined"
color="error"
>
Smazat Smazat
</Button> </Button>
)} )}

View File

@@ -157,7 +157,7 @@ export default function WarehouseInventory() {
/> />
</Box> </Box>
{statusFilter && ( {statusFilter && (
<Button variant="outlined" onClick={resetFilters}> <Button variant="outlined" color="inherit" onClick={resetFilters}>
Zrušit filtry Zrušit filtry
</Button> </Button>
)} )}

View File

@@ -248,7 +248,7 @@ export default function WarehouseIssues() {
/> />
</Box> </Box>
{hasActiveFilters && ( {hasActiveFilters && (
<Button variant="outlined" onClick={resetFilters}> <Button variant="outlined" color="inherit" onClick={resetFilters}>
Zrušit filtry Zrušit filtry
</Button> </Button>
)} )}

View File

@@ -247,7 +247,7 @@ export default function WarehouseReceipts() {
/> />
</Box> </Box>
{hasActiveFilters && ( {hasActiveFilters && (
<Button variant="outlined" onClick={resetFilters}> <Button variant="outlined" color="inherit" onClick={resetFilters}>
Zrušit filtry Zrušit filtry
</Button> </Button>
)} )}

View File

@@ -376,7 +376,7 @@ export default function WarehouseReservations() {
</Box> </Box>
)} )}
{hasActiveFilters && ( {hasActiveFilters && (
<Button variant="outlined" onClick={resetFilters}> <Button variant="outlined" color="inherit" onClick={resetFilters}>
Zrušit filtry Zrušit filtry
</Button> </Button>
)} )}

View File

@@ -7,6 +7,19 @@ const FONT_MONO = "'DM Mono', Menlo, monospace";
// Standard Material easing (matches the legacy --transition cubic-bezier). // Standard Material easing (matches the legacy --transition cubic-bezier).
const EASE = "cubic-bezier(0.4, 0, 0.2, 1)"; const EASE = "cubic-bezier(0.4, 0, 0.2, 1)";
// Dark-mode fills for FILLED colored surfaces (contained buttons, filled chips,
// StatCard badges). The palette .main stays BRIGHT in dark mode (it drives text,
// outlined buttons and row tints), but a bright fill makes WHITE text/glyphs
// illegible — so filled surfaces drop to these darker shades (≈ the light-scheme
// values) in dark mode. One rule everywhere: filled colored = white text,
// readable in both themes. Light mode needs no override (its .main is dark).
export const FILLED_DARK_BG = {
error: { bg: "#dc2626", hover: "#b91c1c" },
success: { bg: "#15803d", hover: "#166534" },
warning: { bg: "#b45309", hover: "#92400e" },
info: { bg: "#1d4ed8", hover: "#1e40af" },
} as const;
export const theme = createTheme({ export const theme = createTheme({
cssVariables: { cssVariables: {
colorSchemeSelector: "[data-theme='%s']", colorSchemeSelector: "[data-theme='%s']",
@@ -80,6 +93,47 @@ export const theme = createTheme({
}, },
"&:active": { transform: "translateY(0) scale(0.97)" }, "&:active": { transform: "translateY(0) scale(0.97)" },
}, },
// Filled colored buttons: WHITE text in BOTH themes (was per-scheme
// contrastText → near-black text on colored fills in dark mode, which
// also clashed with the always-white primary — "black text on one red
// button, white on another"). In dark mode the fill drops to a darker
// shade so white stays legible.
containedError: ({ theme }) => ({
color: "#fff",
...theme.applyStyles("dark", {
"&:not(.Mui-disabled)": {
backgroundColor: FILLED_DARK_BG.error.bg,
"&:hover": { backgroundColor: FILLED_DARK_BG.error.hover },
},
}),
}),
containedSuccess: ({ theme }) => ({
color: "#fff",
...theme.applyStyles("dark", {
"&:not(.Mui-disabled)": {
backgroundColor: FILLED_DARK_BG.success.bg,
"&:hover": { backgroundColor: FILLED_DARK_BG.success.hover },
},
}),
}),
containedWarning: ({ theme }) => ({
color: "#fff",
...theme.applyStyles("dark", {
"&:not(.Mui-disabled)": {
backgroundColor: FILLED_DARK_BG.warning.bg,
"&:hover": { backgroundColor: FILLED_DARK_BG.warning.hover },
},
}),
}),
containedInfo: ({ theme }) => ({
color: "#fff",
...theme.applyStyles("dark", {
"&:not(.Mui-disabled)": {
backgroundColor: FILLED_DARK_BG.info.bg,
"&:hover": { backgroundColor: FILLED_DARK_BG.info.hover },
},
}),
}),
}, },
}, },
MuiCard: { MuiCard: {
@@ -99,6 +153,54 @@ export const theme = createTheme({
fontWeight: 700, fontWeight: 700,
transition: `background-color 200ms ${EASE}, box-shadow 200ms ${EASE}`, transition: `background-color 200ms ${EASE}, box-shadow 200ms ${EASE}`,
}, },
// Filled colored chips follow the same rule as filled buttons: white
// label in both themes, darker fill in dark mode so white stays legible.
// (Chip has no per-color `filledX` key, so target the color class and
// scope to the filled variant.)
colorError: ({ theme }) => ({
"&.MuiChip-filled": {
color: "#fff",
...theme.applyStyles("dark", {
backgroundColor: FILLED_DARK_BG.error.bg,
"&.MuiChip-clickable:hover": {
backgroundColor: FILLED_DARK_BG.error.hover,
},
}),
},
}),
colorSuccess: ({ theme }) => ({
"&.MuiChip-filled": {
color: "#fff",
...theme.applyStyles("dark", {
backgroundColor: FILLED_DARK_BG.success.bg,
"&.MuiChip-clickable:hover": {
backgroundColor: FILLED_DARK_BG.success.hover,
},
}),
},
}),
colorWarning: ({ theme }) => ({
"&.MuiChip-filled": {
color: "#fff",
...theme.applyStyles("dark", {
backgroundColor: FILLED_DARK_BG.warning.bg,
"&.MuiChip-clickable:hover": {
backgroundColor: FILLED_DARK_BG.warning.hover,
},
}),
},
}),
colorInfo: ({ theme }) => ({
"&.MuiChip-filled": {
color: "#fff",
...theme.applyStyles("dark", {
backgroundColor: FILLED_DARK_BG.info.bg,
"&.MuiChip-clickable:hover": {
backgroundColor: FILLED_DARK_BG.info.hover,
},
}),
},
}),
}, },
}, },
MuiOutlinedInput: { MuiOutlinedInput: {

View File

@@ -2,6 +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";
export type StatCardColor = export type StatCardColor =
| "default" | "default"
@@ -31,16 +32,19 @@ export default function StatCard({
footer, footer,
}: StatCardProps) { }: StatCardProps) {
// Solid tile + white glyph: high-contrast in both themes (the old light tint // Solid tile + white glyph: high-contrast in both themes (the old light tint
// left the icon dark/low-visibility, esp. for the default variant). // 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 iconBg = color === "default" ? "grey.600" : `${color}.main`;
const iconColor = "common.white"; 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={{ sx={(theme) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -48,9 +52,12 @@ export default function StatCard({
height: 40, height: 40,
borderRadius: 2, borderRadius: 2,
bgcolor: iconBg, bgcolor: iconBg,
color: iconColor, color: "common.white",
flexShrink: 0, flexShrink: 0,
}} ...(iconDarkBg
? theme.applyStyles("dark", { backgroundColor: iconDarkBg })
: {}),
})}
> >
{icon} {icon}
</Box> </Box>