refactor(css): migrate base.css to MUI GlobalStyles and Leaflet to styled()

Phase 2 of the "fully MUI" cleanup — eliminates two more stylesheets.

- base.css -> src/admin/GlobalStyles.tsx: the reset, typography,
  scrollbar/::selection, theme cross-fade timing and all utility
  classes now live in a single theme-aware MUI <GlobalStyles>, rendered
  inside MuiProvider so every rule resolves against theme.vars (verified
  reactive in both schemes). base.css deleted.
- attendance.css -> a styled("div") LocationMap in AttendanceLocation;
  attendance.css deleted.
- App.tsx bootstrap loader is now fully self-contained (inline spinner
  + keyframes, theme-aware background read from the data-theme attr),
  since it renders before MuiProvider/GlobalStyles mount — it no longer
  depends on the removed .admin-spinner / var(--bg-primary).

tsc -b --noEmit and npm run build clean; verified in Chrome (light +
dark): body bg, text, fonts, .text-warning/.link-accent all correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 17:29:16 +02:00
parent 96e35df535
commit 2a228ea659
7 changed files with 199 additions and 350 deletions

View File

@@ -2,7 +2,13 @@ import { Suspense } from "react";
import { Routes, Route } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import AdminApp from "./admin/AdminApp"; import AdminApp from "./admin/AdminApp";
// Bootstrap loader: renders before MuiProvider (and its global styles) mount,
// so it must be fully self-contained — no theme vars, no global CSS classes.
// Background follows the persisted theme attribute set by ThemeContext.
function AdminLoader() { function AdminLoader() {
const light =
typeof document !== "undefined" &&
document.documentElement.getAttribute("data-theme") === "light";
return ( return (
<div <div
style={{ style={{
@@ -10,10 +16,20 @@ function AdminLoader() {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
background: "var(--bg-primary)", background: light ? "#f4f3f1" : "#0f0f0f",
}} }}
> >
<div className="admin-spinner" /> <style>{"@keyframes boha-boot-spin{to{transform:rotate(360deg)}}"}</style>
<div
style={{
width: 32,
height: 32,
border: "2px solid #d63031",
borderTopColor: "transparent",
borderRadius: "50%",
animation: "boha-boot-spin 0.8s linear infinite",
}}
/>
</div> </div>
); );
} }

View File

@@ -12,8 +12,6 @@ import { LoadingState } from "./ui";
import Login from "./pages/Login"; import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard";
import "./variables.css"; import "./variables.css";
import "./base.css";
import "./attendance.css";
import "./offers.css"; import "./offers.css";
const Users = lazy(() => import("./pages/Users")); const Users = lazy(() => import("./pages/Users"));

162
src/admin/GlobalStyles.tsx Normal file
View File

@@ -0,0 +1,162 @@
import GlobalStyles from "@mui/material/GlobalStyles";
import { fonts } from "./theme";
const EASE = "cubic-bezier(0.4, 0, 0.2, 1)";
/**
* App-wide global styles, theme-aware via MUI's CSS variables.
*
* This replaces the former hand-written `base.css` (reset, typography,
* scrollbar, ::selection, the theme cross-fade timing, utility classes).
* It is rendered once inside MuiProvider so every rule resolves against the
* active color scheme (`theme.vars.*`). The pre-React bootstrap spinner lives
* inline in App.tsx instead, because it renders before MUI mounts.
*/
export default function AppGlobalStyles() {
return (
<GlobalStyles
styles={(theme) => ({
// ---- Reset ----
"*": { margin: 0, padding: 0, boxSizing: "border-box" },
html: { scrollBehavior: "smooth", overflowX: "hidden" },
"html, body, #root": { minHeight: "100dvh", maxWidth: "100vw" },
body: {
fontFamily: fonts.body,
fontSize: "16px",
lineHeight: 1.6,
color: theme.vars!.palette.text.primary,
background: theme.vars!.palette.background.default,
overflowX: "hidden",
overscrollBehaviorX: "none",
WebkitFontSmoothing: "antialiased",
MozOsxFontSmoothing: "grayscale",
transition: "background-color 0.3s ease, color 0.3s ease",
},
"#root": { overflowX: "hidden", touchAction: "pan-y pinch-zoom" },
// ---- Theme cross-fade (View Transitions API, see ThemeContext) ----
"::view-transition-old(root), ::view-transition-new(root)": {
animationDuration: "0.3s",
animationTimingFunction: EASE,
},
// ---- Typography ----
"h1, h2, h3, h4, h5, h6": {
fontFamily: fonts.heading,
fontWeight: 700,
lineHeight: 1.2,
color: theme.vars!.palette.text.primary,
},
h1: { fontSize: "clamp(2.5rem, 5vw, 4rem)" },
h2: { fontSize: "clamp(2rem, 4vw, 3rem)" },
h3: { fontSize: "clamp(1.25rem, 2vw, 1.5rem)" },
p: { color: theme.vars!.palette.text.secondary, lineHeight: 1.6 },
a: {
color: "inherit",
textDecoration: "none",
transition: `all 0.3s ${EASE}`,
},
img: { maxWidth: "100%", height: "auto" },
// ---- Scrollbar & selection ----
"::-webkit-scrollbar": { width: "8px" },
"::-webkit-scrollbar-track": {
background: theme.vars!.palette.background.paper,
},
"::-webkit-scrollbar-thumb": {
background: theme.vars!.palette.divider,
borderRadius: "4px",
},
"::-webkit-scrollbar-thumb:hover": {
background: theme.vars!.palette.text.secondary,
},
"::selection": {
background: theme.vars!.palette.primary.main,
color: "#fff",
},
// ---- Utility classes (legacy, still used across pages) ----
".text-warning": {
color: `${theme.vars!.palette.warning.main} !important`,
},
".text-danger": {
color: `${theme.vars!.palette.error.main} !important`,
},
".text-success": {
color: `${theme.vars!.palette.success.main} !important`,
},
".text-muted": {
color: `${theme.vars!.palette.text.secondary} !important`,
},
".text-secondary": {
color: `${theme.vars!.palette.text.secondary} !important`,
},
".text-tertiary": {
color: `${theme.vars!.palette.text.disabled} !important`,
},
".text-accent": {
color: `${theme.vars!.palette.primary.main} !important`,
},
".fw-500": { fontWeight: 500 },
".fw-600": { fontWeight: "600 !important" },
".link-accent": {
color: theme.vars!.palette.primary.main,
fontWeight: 500,
textDecoration: "none",
},
".link-accent:hover": { textDecoration: "underline" },
".flex-1": { flex: 1 },
".flex-row": { display: "flex", alignItems: "center" },
".flex-row-gap": {
display: "flex",
alignItems: "center",
gap: "0.75rem",
},
".flex-between": {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
},
".inline-flex": { display: "inline-flex", alignItems: "center" },
".mb-1": { marginBottom: "0.25rem" },
".mb-2": { marginBottom: "0.5rem" },
".mb-3": { marginBottom: "0.75rem" },
".mb-4": { marginBottom: "1rem" },
".mb-6": { marginBottom: "1.5rem" },
".mt-1": { marginTop: "0.25rem" },
".mt-2": { marginTop: "0.5rem" },
".mt-3": { marginTop: "0.75rem" },
".mt-6": { marginTop: "1.5rem" },
".gap-1": { gap: "0.25rem" },
".gap-2": { gap: "0.5rem" },
".gap-3": { gap: "0.75rem" },
".gap-4": { gap: "1rem" },
".gap-5": { gap: "1.25rem" },
".gap-6": { gap: "1.5rem" },
".text-right": { textAlign: "right" },
".text-center": { textAlign: "center" },
".text-xs": { fontSize: "0.75rem" },
".text-sm": { fontSize: "0.8125rem" },
".text-md": { fontSize: "0.875rem" },
".text-base": { fontSize: "1rem" },
".w-full": { width: "100%" },
".max-w-xs": { maxWidth: "120px" },
".max-w-sm": { maxWidth: "200px" },
".whitespace-nowrap": { whiteSpace: "nowrap" },
// ---- Reduced motion ----
"@media (prefers-reduced-motion: reduce)": {
"::view-transition-old(root), ::view-transition-new(root)": {
animation: "none",
},
"*, *::before, *::after": {
animationDuration: "0.01ms !important",
animationIterationCount: "1 !important",
transitionDuration: "0.01ms !important",
scrollBehavior: "auto !important",
},
},
})}
/>
);
}

View File

@@ -1,25 +0,0 @@
/* ============================================================================
Attendance Module — Leaflet map (Location page)
============================================================================ */
/* Location Page */
.attendance-location-map {
height: 400px;
border-radius: var(--border-radius-sm);
margin-bottom: 1.5rem;
background: var(--bg-secondary);
position: relative;
z-index: 0; /* stacking context - Leaflet z-indexy zustanou uvnitr */
}
/* ============================================================================
Mobile (responsive)
============================================================================ */
/* The Leaflet map at a fixed 400px dominates a phone viewport — let it scale
with the viewport height (with sensible min/max) on small screens. */
@media (max-width: 640px) {
.attendance-location-map {
height: clamp(220px, 45vh, 400px);
}
}

View File

@@ -1,317 +0,0 @@
/* ============================================================================
Reset & Base
============================================================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
overflow-x: hidden;
}
/* Premium dark/light cross-fade via the View Transitions API (driven by
ThemeContext.toggleTheme). The browser cross-fades a GPU-composited snapshot
of the whole page — smooth at any element count — instead of repainting every
node's colors (which stutters). Tune only the root cross-fade timing here. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}
html,
body,
#root {
min-height: 100%;
min-height: 100dvh;
max-width: 100vw;
}
body {
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-primary);
overflow-x: hidden;
overscroll-behavior-x: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition:
background-color 0.3s ease,
color 0.3s ease;
}
#root {
overflow-x: hidden;
touch-action: pan-y pinch-zoom;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-heading);
font-weight: 700;
line-height: 1.2;
color: var(--text-primary);
}
h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
}
h2 {
font-size: clamp(2rem, 4vw, 3rem);
}
h3 {
font-size: clamp(1.25rem, 2vw, 1.5rem);
}
p {
color: var(--text-secondary);
line-height: 1.6;
}
a {
color: inherit;
text-decoration: none;
transition: var(--transition);
}
img {
max-width: 100%;
height: auto;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
::selection {
background: var(--accent-color);
color: #fff;
}
/* ============================================================================
Base / Utilities
============================================================================ */
.text-warning {
color: var(--warning) !important;
}
.text-danger {
color: var(--danger) !important;
}
.text-success {
color: var(--success) !important;
}
.text-muted {
color: var(--text-muted) !important;
}
.text-secondary {
color: var(--text-secondary) !important;
}
.text-tertiary {
color: var(--text-tertiary) !important;
}
.text-accent {
color: var(--accent-color) !important;
}
.fw-600 {
font-weight: 600 !important;
}
.link-accent {
color: var(--accent-color);
font-weight: 500;
text-decoration: none;
}
.link-accent:hover {
text-decoration: underline;
}
/* Layout utilities */
.flex-1 {
flex: 1;
}
.flex-row {
display: flex;
align-items: center;
}
.flex-row-gap {
display: flex;
align-items: center;
gap: var(--space-3);
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Spacing utilities */
.mb-2 {
margin-bottom: var(--space-2);
}
.mb-4 {
margin-bottom: var(--space-4);
}
.mb-6 {
margin-bottom: var(--space-6);
}
.mt-2 {
margin-top: var(--space-2);
}
.mt-6 {
margin-top: var(--space-6);
}
.gap-2 {
gap: var(--space-2);
}
.gap-4 {
gap: var(--space-4);
}
.gap-5 {
gap: var(--space-5);
}
/* Typography utilities */
.fw-500 {
font-weight: 500;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
/* Loading & Animations */
.admin-spinner {
width: 32px;
height: 32px;
border: 2px solid var(--accent-color);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ── Additional Utilities ─────────────────────────────────────────── */
/* Font sizes */
.text-xs {
font-size: 0.75rem;
}
.text-sm {
font-size: 0.8125rem;
}
.text-md {
font-size: 0.875rem;
}
.text-base {
font-size: 1rem;
}
/* Width utilities */
.w-full {
width: 100%;
}
.max-w-xs {
max-width: 120px;
}
.max-w-sm {
max-width: 200px;
}
/* Whitespace */
.whitespace-nowrap {
white-space: nowrap;
}
/* Additional gaps */
.gap-1 {
gap: 0.25rem;
}
.gap-3 {
gap: 0.75rem;
}
.gap-6 {
gap: 1.5rem;
}
/* Additional margins */
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mt-1 {
margin-top: 0.25rem;
}
.mt-3 {
margin-top: 0.75rem;
}
/* Display */
.inline-flex {
display: inline-flex;
align-items: center;
}
/* Prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.admin-spinner {
animation-duration: 0.8s !important;
animation-iteration-count: infinite !important;
}
}

View File

@@ -6,10 +6,25 @@ import Forbidden from "../components/Forbidden";
import { useNavigate, useParams, Link as RouterLink } from "react-router-dom"; import { useNavigate, useParams, 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 { styled } from "@mui/material/styles";
import L from "leaflet"; import L from "leaflet";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
// Leaflet map container (was the .attendance-location-map rule). zIndex:0
// creates a stacking context so Leaflet's internal z-indexes stay contained.
const LocationMap = styled("div")(({ theme }) => ({
height: "400px",
borderRadius: "8px",
marginBottom: "1.5rem",
background: theme.vars!.palette.background.paper,
position: "relative",
zIndex: 0,
"@media (max-width:640px)": {
height: "clamp(220px, 45vh, 400px)",
},
}));
import { formatDate, formatTime } from "../utils/attendanceHelpers"; import { formatDate, formatTime } from "../utils/attendanceHelpers";
import { import {
attendanceLocationOptions, attendanceLocationOptions,
@@ -209,10 +224,8 @@ export default function AttendanceLocation() {
{record.user_name} {formatDate(record.shift_date)} {record.user_name} {formatDate(record.shift_date)}
</Typography> </Typography>
{/* Leaflet map — kept entirely as-is */} {/* Leaflet map */}
{hasAnyLocation && ( {hasAnyLocation && <LocationMap ref={mapRef} />}
<div ref={mapRef} className="attendance-location-map" />
)}
{/* Location detail grid */} {/* Location detail grid */}
<Box <Box

View File

@@ -4,10 +4,12 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { cs } from "date-fns/locale"; import { cs } from "date-fns/locale";
import { theme } from "../theme"; import { theme } from "../theme";
import AppGlobalStyles from "../GlobalStyles";
export default function MuiProvider({ children }: { children: ReactNode }) { export default function MuiProvider({ children }: { children: ReactNode }) {
return ( return (
<ThemeProvider theme={theme} defaultMode="dark"> <ThemeProvider theme={theme} defaultMode="dark">
<AppGlobalStyles />
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={cs}> <LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={cs}>
{children} {children}
</LocalizationProvider> </LocalizationProvider>