From 2a228ea659dc97c9ee5affbd92cbe7baa3e954be Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 17:29:16 +0200 Subject: [PATCH] refactor(css): migrate base.css to MUI GlobalStyles and Leaflet to styled() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 , 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) --- src/App.tsx | 20 +- src/admin/AdminApp.tsx | 2 - src/admin/GlobalStyles.tsx | 162 +++++++++++++ src/admin/attendance.css | 25 -- src/admin/base.css | 317 ------------------------- src/admin/pages/AttendanceLocation.tsx | 21 +- src/admin/ui/MuiProvider.tsx | 2 + 7 files changed, 199 insertions(+), 350 deletions(-) create mode 100644 src/admin/GlobalStyles.tsx delete mode 100644 src/admin/attendance.css delete mode 100644 src/admin/base.css diff --git a/src/App.tsx b/src/App.tsx index 175d20d..fc97ace 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,13 @@ import { Suspense } from "react"; import { Routes, Route } from "react-router-dom"; 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() { + const light = + typeof document !== "undefined" && + document.documentElement.getAttribute("data-theme") === "light"; return (
-
+ +
); } diff --git a/src/admin/AdminApp.tsx b/src/admin/AdminApp.tsx index cb4c629..726a970 100644 --- a/src/admin/AdminApp.tsx +++ b/src/admin/AdminApp.tsx @@ -12,8 +12,6 @@ import { LoadingState } from "./ui"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import "./variables.css"; -import "./base.css"; -import "./attendance.css"; import "./offers.css"; const Users = lazy(() => import("./pages/Users")); diff --git a/src/admin/GlobalStyles.tsx b/src/admin/GlobalStyles.tsx new file mode 100644 index 0000000..cbe0d4f --- /dev/null +++ b/src/admin/GlobalStyles.tsx @@ -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 ( + ({ + // ---- 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", + }, + }, + })} + /> + ); +} diff --git a/src/admin/attendance.css b/src/admin/attendance.css deleted file mode 100644 index b04edd4..0000000 --- a/src/admin/attendance.css +++ /dev/null @@ -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); - } -} diff --git a/src/admin/base.css b/src/admin/base.css deleted file mode 100644 index 3fe47b1..0000000 --- a/src/admin/base.css +++ /dev/null @@ -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; - } -} diff --git a/src/admin/pages/AttendanceLocation.tsx b/src/admin/pages/AttendanceLocation.tsx index b8ed5e5..c6736ee 100644 --- a/src/admin/pages/AttendanceLocation.tsx +++ b/src/admin/pages/AttendanceLocation.tsx @@ -6,10 +6,25 @@ import Forbidden from "../components/Forbidden"; import { useNavigate, useParams, Link as RouterLink } from "react-router-dom"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import { styled } from "@mui/material/styles"; import L from "leaflet"; 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 { attendanceLocationOptions, @@ -209,10 +224,8 @@ export default function AttendanceLocation() { {record.user_name} — {formatDate(record.shift_date)} - {/* Leaflet map — kept entirely as-is */} - {hasAnyLocation && ( -
- )} + {/* Leaflet map */} + {hasAnyLocation && } {/* Location detail grid */} + {children}