feat(shell): scrollable status tabs, per-page tab titles, skip-to-content, Czech MUI locale
- Shared Tabs: variant=scrollable + auto scroll buttons — the 5 status-filter
tabs no longer clip unreachable at ~360px (desktop rendering unchanged).
- TitleSync: browser tab shows the active page ('Faktury · BOHA'; ambiguous
labels qualified with their section, e.g. 'Záznam – Docházka · BOHA');
index.html fallback now 'BOHA Admin'.
- Skip-to-content link (visually hidden, visible on focus) + id/tabIndex on
<main> — keyboard users skip the 248px sidebar.
- MUI csCZ locale merged into the theme (kills built-in English strings like
'No options'); Czech noOptionsText on Customer/Supplier pickers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
type PaletteColor,
|
||||
type PaletteColorChannel,
|
||||
} from "@mui/material/styles";
|
||||
import { csCZ } from "@mui/material/locale";
|
||||
|
||||
const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif";
|
||||
const FONT_HEADING = "'Urbanist', sans-serif";
|
||||
@@ -25,257 +26,265 @@ export const FILLED_DARK_BG = {
|
||||
info: { bg: "#1d4ed8", hover: "#1e40af" },
|
||||
} as const;
|
||||
|
||||
export const theme = createTheme({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: "[data-theme='%s']",
|
||||
},
|
||||
colorSchemes: {
|
||||
light: {
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: { main: "#c73030" },
|
||||
success: { main: "#15803d", contrastText: "#fff" },
|
||||
warning: { main: "#b45309", contrastText: "#fff" },
|
||||
error: { main: "#b91c1c", contrastText: "#fff" },
|
||||
info: { main: "#1d4ed8", contrastText: "#fff" },
|
||||
// Sidebar section hues (full objects so cssVariables emits *Channel
|
||||
// tokens — the nav tiles use channel-alpha washes).
|
||||
teal: {
|
||||
main: "#0e8a7c",
|
||||
light: "#3aa99c",
|
||||
dark: "#0a675d",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
violet: {
|
||||
main: "#6a4cb4",
|
||||
light: "#8a6fd0",
|
||||
dark: "#54399a",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
slate: {
|
||||
main: "#5c6470",
|
||||
light: "#7d8694",
|
||||
dark: "#454c56",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
background: { default: "#f4f3f1", paper: "#ffffff" },
|
||||
text: { primary: "#1a1a1a", secondary: "#555555" },
|
||||
divider: "rgba(0,0,0,0.1)",
|
||||
},
|
||||
// The theme composes MUI's Czech component localization (csCZ) as a second
|
||||
// createTheme argument (the supported deep-merge form). It only injects
|
||||
// component defaultProps — Czech built-in strings for Autocomplete
|
||||
// ("Žádné možnosti"), TablePagination, Alert, Pagination, … — and does not
|
||||
// touch the palette / cssVariables config.
|
||||
export const theme = createTheme(
|
||||
{
|
||||
cssVariables: {
|
||||
colorSchemeSelector: "[data-theme='%s']",
|
||||
},
|
||||
dark: {
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: { main: "#d63031" },
|
||||
success: { main: "#22c55e", contrastText: "#1a1a1a" },
|
||||
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
||||
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
||||
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
|
||||
// Sidebar section hues — brighter mains so the glyphs read on dark.
|
||||
teal: {
|
||||
main: "#2dd4bf",
|
||||
light: "#5eead4",
|
||||
dark: "#14b8a6",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
violet: {
|
||||
main: "#a78bfa",
|
||||
light: "#c4b5fd",
|
||||
dark: "#8b5cf6",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
slate: {
|
||||
main: "#94a3b8",
|
||||
light: "#cbd5e1",
|
||||
dark: "#64748b",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
background: { default: "#0f0f0f", paper: "#1a1a1a" },
|
||||
text: { primary: "#ffffff", secondary: "#a0a0a0" },
|
||||
divider: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: { borderRadius: 10 },
|
||||
typography: {
|
||||
fontFamily: FONT_BODY,
|
||||
h1: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h2: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h3: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h4: {
|
||||
fontFamily: FONT_HEADING,
|
||||
fontWeight: 700,
|
||||
// Page/detail headlines: MUI's default 2.125rem overflows phone
|
||||
// viewports (long document titles + number). Scale down on xs only.
|
||||
"@media (max-width:600px)": { fontSize: "1.5rem" },
|
||||
},
|
||||
h5: { fontFamily: FONT_HEADING, fontWeight: 700 },
|
||||
h6: { fontFamily: FONT_HEADING, fontWeight: 700 },
|
||||
button: { textTransform: "none", fontWeight: 600 },
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
defaultProps: { disableElevation: true },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 999,
|
||||
paddingInline: "0.95rem",
|
||||
// Snappy press (150ms) + smooth color/shadow (250ms).
|
||||
transition: [
|
||||
`background-color 250ms ${EASE}`,
|
||||
`border-color 250ms ${EASE}`,
|
||||
`color 200ms ${EASE}`,
|
||||
`box-shadow 250ms ${EASE}`,
|
||||
`transform 150ms ${EASE}`,
|
||||
`filter 200ms ${EASE}`,
|
||||
].join(", "),
|
||||
"&:active": { transform: "scale(0.97)" },
|
||||
"@media (prefers-reduced-motion: reduce)": { transition: "none" },
|
||||
},
|
||||
containedPrimary: {
|
||||
backgroundImage: "linear-gradient(135deg, #e23a3a, #c01f1f)",
|
||||
boxShadow: "0 5px 14px rgba(214,48,49,0.32)",
|
||||
// The gradient can't transition, so animate lift + glow instead.
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
filter: "brightness(1.04)",
|
||||
boxShadow: "0 8px 20px rgba(214,48,49,0.42)",
|
||||
colorSchemes: {
|
||||
light: {
|
||||
palette: {
|
||||
mode: "light",
|
||||
primary: { main: "#c73030" },
|
||||
success: { main: "#15803d", contrastText: "#fff" },
|
||||
warning: { main: "#b45309", contrastText: "#fff" },
|
||||
error: { main: "#b91c1c", contrastText: "#fff" },
|
||||
info: { main: "#1d4ed8", contrastText: "#fff" },
|
||||
// Sidebar section hues (full objects so cssVariables emits *Channel
|
||||
// tokens — the nav tiles use channel-alpha washes).
|
||||
teal: {
|
||||
main: "#0e8a7c",
|
||||
light: "#3aa99c",
|
||||
dark: "#0a675d",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
"&:active": { transform: "translateY(0) scale(0.97)" },
|
||||
// Honor reduced-motion like MuiButton.root / MuiOutlinedInput: drop
|
||||
// the lift/press transform (glow + brightness stay, they don't move).
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
"&:hover": { transform: "none" },
|
||||
"&:active": { transform: "none" },
|
||||
violet: {
|
||||
main: "#6a4cb4",
|
||||
light: "#8a6fd0",
|
||||
dark: "#54399a",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
slate: {
|
||||
main: "#5c6470",
|
||||
light: "#7d8694",
|
||||
dark: "#454c56",
|
||||
contrastText: "#fff",
|
||||
},
|
||||
background: { default: "#f4f3f1", paper: "#ffffff" },
|
||||
text: { primary: "#1a1a1a", secondary: "#555555" },
|
||||
divider: "rgba(0,0,0,0.1)",
|
||||
},
|
||||
// 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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 16,
|
||||
boxShadow:
|
||||
"0 6px 20px rgba(20,20,40,0.06), 0 1px 2px rgba(0,0,0,0.03)",
|
||||
transition: `box-shadow 250ms ${EASE}, transform 250ms ${EASE}`,
|
||||
dark: {
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: { main: "#d63031" },
|
||||
success: { main: "#22c55e", contrastText: "#1a1a1a" },
|
||||
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
||||
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
||||
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
|
||||
// Sidebar section hues — brighter mains so the glyphs read on dark.
|
||||
teal: {
|
||||
main: "#2dd4bf",
|
||||
light: "#5eead4",
|
||||
dark: "#14b8a6",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
violet: {
|
||||
main: "#a78bfa",
|
||||
light: "#c4b5fd",
|
||||
dark: "#8b5cf6",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
slate: {
|
||||
main: "#94a3b8",
|
||||
light: "#cbd5e1",
|
||||
dark: "#64748b",
|
||||
contrastText: "#1a1a1a",
|
||||
},
|
||||
background: { default: "#0f0f0f", paper: "#1a1a1a" },
|
||||
text: { primary: "#ffffff", secondary: "#a0a0a0" },
|
||||
divider: "rgba(255,255,255,0.08)",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 999,
|
||||
fontWeight: 700,
|
||||
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": {
|
||||
shape: { borderRadius: 10 },
|
||||
typography: {
|
||||
fontFamily: FONT_BODY,
|
||||
h1: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h2: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h3: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||
h4: {
|
||||
fontFamily: FONT_HEADING,
|
||||
fontWeight: 700,
|
||||
// Page/detail headlines: MUI's default 2.125rem overflows phone
|
||||
// viewports (long document titles + number). Scale down on xs only.
|
||||
"@media (max-width:600px)": { fontSize: "1.5rem" },
|
||||
},
|
||||
h5: { fontFamily: FONT_HEADING, fontWeight: 700 },
|
||||
h6: { fontFamily: FONT_HEADING, fontWeight: 700 },
|
||||
button: { textTransform: "none", fontWeight: 600 },
|
||||
},
|
||||
components: {
|
||||
MuiButton: {
|
||||
defaultProps: { disableElevation: true },
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 999,
|
||||
paddingInline: "0.95rem",
|
||||
// Snappy press (150ms) + smooth color/shadow (250ms).
|
||||
transition: [
|
||||
`background-color 250ms ${EASE}`,
|
||||
`border-color 250ms ${EASE}`,
|
||||
`color 200ms ${EASE}`,
|
||||
`box-shadow 250ms ${EASE}`,
|
||||
`transform 150ms ${EASE}`,
|
||||
`filter 200ms ${EASE}`,
|
||||
].join(", "),
|
||||
"&:active": { transform: "scale(0.97)" },
|
||||
"@media (prefers-reduced-motion: reduce)": { transition: "none" },
|
||||
},
|
||||
containedPrimary: {
|
||||
backgroundImage: "linear-gradient(135deg, #e23a3a, #c01f1f)",
|
||||
boxShadow: "0 5px 14px rgba(214,48,49,0.32)",
|
||||
// The gradient can't transition, so animate lift + glow instead.
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
filter: "brightness(1.04)",
|
||||
boxShadow: "0 8px 20px rgba(214,48,49,0.42)",
|
||||
},
|
||||
"&:active": { transform: "translateY(0) scale(0.97)" },
|
||||
// Honor reduced-motion like MuiButton.root / MuiOutlinedInput: drop
|
||||
// the lift/press transform (glow + brightness stay, they don't move).
|
||||
"@media (prefers-reduced-motion: reduce)": {
|
||||
"&:hover": { transform: "none" },
|
||||
"&:active": { transform: "none" },
|
||||
},
|
||||
},
|
||||
// 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", {
|
||||
backgroundColor: FILLED_DARK_BG.error.bg,
|
||||
"&.MuiChip-clickable:hover": {
|
||||
backgroundColor: FILLED_DARK_BG.error.hover,
|
||||
"&:not(.Mui-disabled)": {
|
||||
backgroundColor: FILLED_DARK_BG.error.bg,
|
||||
"&:hover": { backgroundColor: FILLED_DARK_BG.error.hover },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
colorSuccess: ({ theme }) => ({
|
||||
"&.MuiChip-filled": {
|
||||
}),
|
||||
containedSuccess: ({ theme }) => ({
|
||||
color: "#fff",
|
||||
...theme.applyStyles("dark", {
|
||||
backgroundColor: FILLED_DARK_BG.success.bg,
|
||||
"&.MuiChip-clickable:hover": {
|
||||
backgroundColor: FILLED_DARK_BG.success.hover,
|
||||
"&:not(.Mui-disabled)": {
|
||||
backgroundColor: FILLED_DARK_BG.success.bg,
|
||||
"&:hover": { backgroundColor: FILLED_DARK_BG.success.hover },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
colorWarning: ({ theme }) => ({
|
||||
"&.MuiChip-filled": {
|
||||
}),
|
||||
containedWarning: ({ theme }) => ({
|
||||
color: "#fff",
|
||||
...theme.applyStyles("dark", {
|
||||
backgroundColor: FILLED_DARK_BG.warning.bg,
|
||||
"&.MuiChip-clickable:hover": {
|
||||
backgroundColor: FILLED_DARK_BG.warning.hover,
|
||||
"&:not(.Mui-disabled)": {
|
||||
backgroundColor: FILLED_DARK_BG.warning.bg,
|
||||
"&:hover": { backgroundColor: FILLED_DARK_BG.warning.hover },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
colorInfo: ({ theme }) => ({
|
||||
"&.MuiChip-filled": {
|
||||
}),
|
||||
containedInfo: ({ theme }) => ({
|
||||
color: "#fff",
|
||||
...theme.applyStyles("dark", {
|
||||
backgroundColor: FILLED_DARK_BG.info.bg,
|
||||
"&.MuiChip-clickable:hover": {
|
||||
backgroundColor: FILLED_DARK_BG.info.hover,
|
||||
"&:not(.Mui-disabled)": {
|
||||
backgroundColor: FILLED_DARK_BG.info.bg,
|
||||
"&:hover": { backgroundColor: FILLED_DARK_BG.info.hover },
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// Soft brand focus ring fades in over 200ms.
|
||||
transition: `box-shadow 200ms ${EASE}`,
|
||||
"&.Mui-focused": {
|
||||
boxShadow: "0 0 0 3px rgba(199, 48, 48, 0.12)",
|
||||
},
|
||||
"@media (prefers-reduced-motion: reduce)": { transition: "none" },
|
||||
}),
|
||||
},
|
||||
notchedOutline: {
|
||||
transition: `border-color 200ms ${EASE}`,
|
||||
},
|
||||
MuiCard: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 16,
|
||||
boxShadow:
|
||||
"0 6px 20px rgba(20,20,40,0.06), 0 1px 2px rgba(0,0,0,0.03)",
|
||||
transition: `box-shadow 250ms ${EASE}, transform 250ms ${EASE}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: 999,
|
||||
fontWeight: 700,
|
||||
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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
// Soft brand focus ring fades in over 200ms.
|
||||
transition: `box-shadow 200ms ${EASE}`,
|
||||
"&.Mui-focused": {
|
||||
boxShadow: "0 0 0 3px rgba(199, 48, 48, 0.12)",
|
||||
},
|
||||
"@media (prefers-reduced-motion: reduce)": { transition: "none" },
|
||||
},
|
||||
notchedOutline: {
|
||||
transition: `border-color 200ms ${EASE}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
csCZ,
|
||||
);
|
||||
|
||||
export const fonts = {
|
||||
body: FONT_BODY,
|
||||
|
||||
Reference in New Issue
Block a user