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:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user