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:
@@ -19,7 +19,7 @@
|
|||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<title>Admin</title>
|
<title>BOHA Admin</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
42
src/admin/components/TitleSync.tsx
Normal file
42
src/admin/components/TitleSync.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { menuSections, isItemActive } from "../ui/navData";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Null-rendering helper that keeps the browser tab title in sync with the
|
||||||
|
* active navigation item. Scans menuSections in order (first match wins) via
|
||||||
|
* the same isItemActive logic the sidebar uses, so detail routes covered by
|
||||||
|
* matchPrefix (e.g. /offers/123) inherit their section item's label.
|
||||||
|
* No cleanup on unmount — the last title simply persists.
|
||||||
|
*/
|
||||||
|
// Labels reused across sections ("Záznam", "Moje historie", "Správa",
|
||||||
|
// "Přehled") would produce identical tab titles — qualify those with their
|
||||||
|
// section label so /attendance and /trips tabs stay distinguishable.
|
||||||
|
const labelCounts = new Map<string, number>();
|
||||||
|
for (const section of menuSections) {
|
||||||
|
for (const item of section.items) {
|
||||||
|
labelCounts.set(item.label, (labelCounts.get(item.label) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TitleSync() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
for (const section of menuSections) {
|
||||||
|
const item = section.items.find((candidate) =>
|
||||||
|
isItemActive(candidate, pathname),
|
||||||
|
);
|
||||||
|
if (item) {
|
||||||
|
const ambiguous = (labelCounts.get(item.label) ?? 0) > 1;
|
||||||
|
document.title = ambiguous
|
||||||
|
? `${item.label} – ${section.label} · BOHA`
|
||||||
|
: `${item.label} · BOHA`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.title = "BOHA Admin";
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type PaletteColor,
|
type PaletteColor,
|
||||||
type PaletteColorChannel,
|
type PaletteColorChannel,
|
||||||
} from "@mui/material/styles";
|
} from "@mui/material/styles";
|
||||||
|
import { csCZ } from "@mui/material/locale";
|
||||||
|
|
||||||
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";
|
||||||
@@ -25,257 +26,265 @@ export const FILLED_DARK_BG = {
|
|||||||
info: { bg: "#1d4ed8", hover: "#1e40af" },
|
info: { bg: "#1d4ed8", hover: "#1e40af" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const theme = createTheme({
|
// The theme composes MUI's Czech component localization (csCZ) as a second
|
||||||
cssVariables: {
|
// createTheme argument (the supported deep-merge form). It only injects
|
||||||
colorSchemeSelector: "[data-theme='%s']",
|
// component defaultProps — Czech built-in strings for Autocomplete
|
||||||
},
|
// ("Žádné možnosti"), TablePagination, Alert, Pagination, … — and does not
|
||||||
colorSchemes: {
|
// touch the palette / cssVariables config.
|
||||||
light: {
|
export const theme = createTheme(
|
||||||
palette: {
|
{
|
||||||
mode: "light",
|
cssVariables: {
|
||||||
primary: { main: "#c73030" },
|
colorSchemeSelector: "[data-theme='%s']",
|
||||||
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)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
dark: {
|
colorSchemes: {
|
||||||
palette: {
|
light: {
|
||||||
mode: "dark",
|
palette: {
|
||||||
primary: { main: "#d63031" },
|
mode: "light",
|
||||||
success: { main: "#22c55e", contrastText: "#1a1a1a" },
|
primary: { main: "#c73030" },
|
||||||
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
success: { main: "#15803d", contrastText: "#fff" },
|
||||||
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
warning: { main: "#b45309", contrastText: "#fff" },
|
||||||
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
|
error: { main: "#b91c1c", contrastText: "#fff" },
|
||||||
// Sidebar section hues — brighter mains so the glyphs read on dark.
|
info: { main: "#1d4ed8", contrastText: "#fff" },
|
||||||
teal: {
|
// Sidebar section hues (full objects so cssVariables emits *Channel
|
||||||
main: "#2dd4bf",
|
// tokens — the nav tiles use channel-alpha washes).
|
||||||
light: "#5eead4",
|
teal: {
|
||||||
dark: "#14b8a6",
|
main: "#0e8a7c",
|
||||||
contrastText: "#1a1a1a",
|
light: "#3aa99c",
|
||||||
},
|
dark: "#0a675d",
|
||||||
violet: {
|
contrastText: "#fff",
|
||||||
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)",
|
|
||||||
},
|
},
|
||||||
"&:active": { transform: "translateY(0) scale(0.97)" },
|
violet: {
|
||||||
// Honor reduced-motion like MuiButton.root / MuiOutlinedInput: drop
|
main: "#6a4cb4",
|
||||||
// the lift/press transform (glow + brightness stay, they don't move).
|
light: "#8a6fd0",
|
||||||
"@media (prefers-reduced-motion: reduce)": {
|
dark: "#54399a",
|
||||||
"&:hover": { transform: "none" },
|
contrastText: "#fff",
|
||||||
"&:active": { transform: "none" },
|
|
||||||
},
|
},
|
||||||
|
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 },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
dark: {
|
||||||
MuiCard: {
|
palette: {
|
||||||
styleOverrides: {
|
mode: "dark",
|
||||||
root: {
|
primary: { main: "#d63031" },
|
||||||
borderRadius: 16,
|
success: { main: "#22c55e", contrastText: "#1a1a1a" },
|
||||||
boxShadow:
|
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
||||||
"0 6px 20px rgba(20,20,40,0.06), 0 1px 2px rgba(0,0,0,0.03)",
|
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
||||||
transition: `box-shadow 250ms ${EASE}, transform 250ms ${EASE}`,
|
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: {
|
shape: { borderRadius: 10 },
|
||||||
styleOverrides: {
|
typography: {
|
||||||
root: {
|
fontFamily: FONT_BODY,
|
||||||
borderRadius: 999,
|
h1: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||||
fontWeight: 700,
|
h2: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||||
transition: `background-color 200ms ${EASE}, box-shadow 200ms ${EASE}`,
|
h3: { fontFamily: FONT_HEADING, fontWeight: 800 },
|
||||||
},
|
h4: {
|
||||||
// Filled colored chips follow the same rule as filled buttons: white
|
fontFamily: FONT_HEADING,
|
||||||
// label in both themes, darker fill in dark mode so white stays legible.
|
fontWeight: 700,
|
||||||
// (Chip has no per-color `filledX` key, so target the color class and
|
// Page/detail headlines: MUI's default 2.125rem overflows phone
|
||||||
// scope to the filled variant.)
|
// viewports (long document titles + number). Scale down on xs only.
|
||||||
colorError: ({ theme }) => ({
|
"@media (max-width:600px)": { fontSize: "1.5rem" },
|
||||||
"&.MuiChip-filled": {
|
},
|
||||||
|
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",
|
color: "#fff",
|
||||||
...theme.applyStyles("dark", {
|
...theme.applyStyles("dark", {
|
||||||
backgroundColor: FILLED_DARK_BG.error.bg,
|
"&:not(.Mui-disabled)": {
|
||||||
"&.MuiChip-clickable:hover": {
|
backgroundColor: FILLED_DARK_BG.error.bg,
|
||||||
backgroundColor: FILLED_DARK_BG.error.hover,
|
"&:hover": { backgroundColor: FILLED_DARK_BG.error.hover },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
}),
|
||||||
}),
|
containedSuccess: ({ theme }) => ({
|
||||||
colorSuccess: ({ theme }) => ({
|
|
||||||
"&.MuiChip-filled": {
|
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
...theme.applyStyles("dark", {
|
...theme.applyStyles("dark", {
|
||||||
backgroundColor: FILLED_DARK_BG.success.bg,
|
"&:not(.Mui-disabled)": {
|
||||||
"&.MuiChip-clickable:hover": {
|
backgroundColor: FILLED_DARK_BG.success.bg,
|
||||||
backgroundColor: FILLED_DARK_BG.success.hover,
|
"&:hover": { backgroundColor: FILLED_DARK_BG.success.hover },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
}),
|
||||||
}),
|
containedWarning: ({ theme }) => ({
|
||||||
colorWarning: ({ theme }) => ({
|
|
||||||
"&.MuiChip-filled": {
|
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
...theme.applyStyles("dark", {
|
...theme.applyStyles("dark", {
|
||||||
backgroundColor: FILLED_DARK_BG.warning.bg,
|
"&:not(.Mui-disabled)": {
|
||||||
"&.MuiChip-clickable:hover": {
|
backgroundColor: FILLED_DARK_BG.warning.bg,
|
||||||
backgroundColor: FILLED_DARK_BG.warning.hover,
|
"&:hover": { backgroundColor: FILLED_DARK_BG.warning.hover },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
}),
|
||||||
}),
|
containedInfo: ({ theme }) => ({
|
||||||
colorInfo: ({ theme }) => ({
|
|
||||||
"&.MuiChip-filled": {
|
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
...theme.applyStyles("dark", {
|
...theme.applyStyles("dark", {
|
||||||
backgroundColor: FILLED_DARK_BG.info.bg,
|
"&:not(.Mui-disabled)": {
|
||||||
"&.MuiChip-clickable:hover": {
|
backgroundColor: FILLED_DARK_BG.info.bg,
|
||||||
backgroundColor: FILLED_DARK_BG.info.hover,
|
"&: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 = {
|
export const fonts = {
|
||||||
body: FONT_BODY,
|
body: FONT_BODY,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { setLogoutAlert } from "../utils/api";
|
|||||||
import SidebarNav from "./SidebarNav";
|
import SidebarNav from "./SidebarNav";
|
||||||
import LoadingState from "./LoadingState";
|
import LoadingState from "./LoadingState";
|
||||||
import ShortcutsHelp from "../components/ShortcutsHelp";
|
import ShortcutsHelp from "../components/ShortcutsHelp";
|
||||||
|
import TitleSync from "../components/TitleSync";
|
||||||
|
|
||||||
const DRAWER_WIDTH = 248;
|
const DRAWER_WIDTH = 248;
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ export default function AppShell() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScopedCssBaseline>
|
<ScopedCssBaseline>
|
||||||
|
<TitleSync />
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.98 }}
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
animate={
|
animate={
|
||||||
@@ -110,6 +112,38 @@ export default function AppShell() {
|
|||||||
ease: [0.4, 0, 0.2, 1],
|
ease: [0.4, 0, 0.2, 1],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Skip link: first focusable element; visually hidden (clipped)
|
||||||
|
until keyboard focus reveals it above the drawer. */}
|
||||||
|
<Box
|
||||||
|
component="a"
|
||||||
|
href="#main-content"
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
zIndex: theme.zIndex.drawer + 2,
|
||||||
|
width: "1px",
|
||||||
|
height: "1px",
|
||||||
|
overflow: "hidden",
|
||||||
|
clipPath: "inset(50%)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
bgcolor: theme.vars!.palette.primary.main,
|
||||||
|
color: theme.vars!.palette.primary.contrastText,
|
||||||
|
textDecoration: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
"&:focus": {
|
||||||
|
width: "auto",
|
||||||
|
height: "auto",
|
||||||
|
overflow: "visible",
|
||||||
|
clipPath: "none",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Přeskočit na obsah
|
||||||
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -209,10 +243,15 @@ export default function AppShell() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
|
id="main-content"
|
||||||
|
tabIndex={-1}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
px: immersiveOnMobile ? { xs: 0, md: 3 } : { xs: 2, md: 3 },
|
px: immersiveOnMobile ? { xs: 0, md: 3 } : { xs: 2, md: 3 },
|
||||||
pb: immersiveOnMobile ? { xs: 0, md: 4 } : 4,
|
pb: immersiveOnMobile ? { xs: 0, md: 4 } : 4,
|
||||||
|
// Skip-link target: focus lands here programmatically — the
|
||||||
|
// region-wide focus ring would be noise, not a signal.
|
||||||
|
"&:focus": { outline: "none" },
|
||||||
...(immersiveOnMobile && {
|
...(immersiveOnMobile && {
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: { xs: "hidden", md: "visible" },
|
overflow: { xs: "hidden", md: "visible" },
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function CustomerPicker({
|
|||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
autoHighlight
|
autoHighlight
|
||||||
|
noOptionsText="Žádní zákazníci"
|
||||||
renderOption={(props, c) => {
|
renderOption={(props, c) => {
|
||||||
const { key, ...rest } = props as typeof props & { key: string };
|
const { key, ...rest } = props as typeof props & { key: string };
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export default function SupplierPicker({
|
|||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
autoHighlight
|
autoHighlight
|
||||||
|
noOptionsText="Žádní dodavatelé"
|
||||||
renderOption={(props, s) => {
|
renderOption={(props, s) => {
|
||||||
const { key, ...rest } = props as typeof props & { key: string };
|
const { key, ...rest } = props as typeof props & { key: string };
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ export function Tabs({
|
|||||||
<MuiTabs
|
<MuiTabs
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(_, v) => onChange(v)}
|
onChange={(_, v) => onChange(v)}
|
||||||
|
// Scrollable so many tabs (e.g. 5 status filters) stay reachable on
|
||||||
|
// ~360px phones instead of clipping. When everything fits (desktop),
|
||||||
|
// scroll buttons don't render and this looks identical to "standard" —
|
||||||
|
// callers' centered flex wrappers keep shrink-wrapping the bar.
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
allowScrollButtonsMobile
|
||||||
sx={{
|
sx={{
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: "divider",
|
borderColor: "divider",
|
||||||
|
|||||||
Reference in New Issue
Block a user