# CSS → MUI Migration — Design Spec **Date:** 2026-06-06 **Status:** Approved design, pending implementation plan **Topic:** Replace hand-written CSS with MUI (Material UI), themed to the existing brand, with a modern "Soft-SaaS + dense tables" look, rolled out incrementally. --- ## 1. Goal Migrate the admin frontend off its ~7,100 lines of hand-written CSS onto **MUI (Material UI)** as the styling system, while simultaneously delivering a **modern visual refresh**. Dark/light theming and the existing design tokens are preserved (they already exist and work). The migration is **incremental and always shippable** — never a frozen big-bang. This is explicitly a **styling + component-layer migration**. Application behavior, data flow, API contracts, and business logic remain identical (one deliberate exception: modal accessibility — see §5 / §11). Backend code is not touched. --- ## 2. Current state (audit findings, 2026-06-06) The premise "we want dark/light mode" is largely **already met**. The audit found: - **19 CSS files, ~7,101 lines** under `src/admin/`. Largest: `components.css` (1,224), `plan.css` (1,152), `offers.css` (641), `layout.css` (582), `dashboard.css` (525), `forms.css` (510), `attendance.css` (455), `base.css` (411). Smallest: `responsive.css` (6 lines, effectively an empty placeholder). The token file `variables.css` (164) and `plan.css` are special-cased in §6. - **A complete CSS-variable token system** in `variables.css`: spacing scale, colors, radii, motion durations, fonts, safe-area insets. - **A full dark + light theme** via `[data-theme="dark"]` / `[data-theme="light"]`. The attribute is owned and written by **`src/context/ThemeContext.tsx`** (`useTheme()` / `toggleTheme()`, which calls `document.documentElement.setAttribute("data-theme", …)`). `Sidebar.tsx`, `AdminLayout.tsx`, and `Login.tsx` only **consume** it via `useTheme()`. - **Distinctive fonts already chosen:** Urbanist (headings), Plus Jakarta Sans (body), DM Mono (mono). - **Stack:** React 18.3, Vite 8, TypeScript (strict), TanStack Query v5, **framer-motion** (used broadly — see §9: modals, toasts, the AdminLayout logout transition, dashboard cards, Login), react-datepicker, react-quill-new, Leaflet, @dnd-kit. No Tailwind / MUI / Emotion direct deps; no PostCSS config (PostCSS ships only transitively under Vite). - **Type skew (pre-existing):** `react`/`react-dom` are `^18.3.1` but `@types/react`/`@types/react-dom` are `^19.x`. MUI's component types are sensitive to the `@types/react` major version — see §11 / §14. **Implication:** the migration is not about adding theming; it's about replacing the CSS plumbing and modernizing the look while carrying the existing tokens forward. --- ## 3. Locked decisions | Decision | Choice | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Goal | Both — re-plumb CSS **and** modern visual refresh | | Approach | Component library (not a utility framework) | | Library | **MUI (Material UI)** — target **v7** + **MUI X v8** (pickers), themed to the brand | | Look | **"Soft-SaaS shell (B) + dense data tables (D)"** — airy modern chrome, dense efficient tables | | Theming | Existing CSS-var tokens become the source; one `data-theme` attribute drives both MUI and legacy CSS (mechanism verified against MUI v7 docs — §4.1) | | Roll-out | Incremental, foundation-first; each phase is one or more normal releases | | Pilot page | **Vozidla** (vehicles) — focused CRUD pilot | | Heavy tables | MUI `Table` styled dense, reusing existing `useTableSort` + `usePaginatedQuery` hooks (no DataGrid, no paid license) | --- ## 4. Architecture The tokens become the **single shared palette** that feeds both worlds during the migration, so migrated and unmigrated pages stay on the same colors/typography. Structure modernizes per page (see §7 for the token-level vs component-level split). ``` Tokens (colors, fonts, radii, motion, dark/light) ← refreshed once, early │ (identical palette feeds both sides) ├───────────────► legacy CSS ── unmigrated pages ┐ │ ├─ coexist, same palette └──► MUI theme (createTheme + colorSchemes) ──────────────┘ │ ▼ Foundation UI kit (src/admin/ui/*) ──► migrated pages ``` ### 4.1 Theming bridge (`src/admin/theme.ts`) - One `theme.ts` builds the MUI theme from the tokens: - **Palette:** `primary` = brand red (`#d63031` dark / `#c73030` light), plus `success`/`warning`/`error`/`info` mapped to existing semantic tokens. - **Typography:** Urbanist for `h1–h6`, Plus Jakarta Sans for body, DM Mono for numeric/monospace contexts. - **Shape/spacing:** `borderRadius` and spacing from the token scale. - **`components` overrides:** the Soft-SaaS defaults (rounded cards, pill buttons, soft shadows, tinted chips) baked in so every MUI component is on-brand by default. - **Dark/light keeps the existing toggle (verified against MUI v7 docs).** MUI's CSS theme variables support a custom color-scheme selector that targets the existing attribute exactly: ```ts createTheme({ cssVariables: { colorSchemeSelector: "[data-theme='%s']" }, colorSchemes: { light: true, dark: true }, // …palette/typography/components }); ``` This emits `[data-theme="light"]{…}` and `[data-theme="dark"]{…}`, matching the attribute `ThemeContext` already writes. One attribute drives MUI _and_ legacy CSS → no second toggle, 1:1 dark parity. - **One attribute owner.** `ThemeContext` remains the sole writer of `data-theme` on ``. Its `toggleTheme()` must **also call MUI's `setMode()`** so MUI's internal color-scheme state stays in sync with the attribute. Do **not** also enable MUI's `InitColorSchemeScript` attribute-writing (avoid two writers). Wired in Phase 0. ### 4.2 Coexistence (the safety mechanism) - Use MUI's **`ScopedCssBaseline`** around migrated page subtrees only, so MUI's CSS reset applies _inside_ migrated pages without disturbing unmigrated ones still on `base.css`. No global reset until teardown. - The `CssVarsProvider`/`ThemeProvider` must sit **above** every `ScopedCssBaseline`, and `data-theme` must live on a common ancestor (``, as today) so the `[data-theme='%s']` selector resolves inside scoped subtrees. - Styling engine: MUI default (**Emotion**). SPA only (Vite, no SSR), so no SSR-cache complexity. --- ## 5. Foundation UI kit (`src/admin/ui/`) **Principle: pages never import `@mui` directly.** A thin wrapper kit bakes in brand defaults and **keeps the current component APIs and Czech labels**, so migrating a page is mostly swapping imports, not rewriting logic, and tuning a default touches one file, not 57. | Today | → Foundation kit (`src/admin/ui/`) | MUI base | | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | `FormModal` / `ConfirmModal` | `` — preserves **FormModal's** full prop set (`submitDisabled`, `subtitle`, `loading`); ConfirmModal needs only `title`/`message`/`type`/`confirmVariant`/`loading` | `Dialog` | | `.admin-table*` + `useTableSort` + `usePaginatedQuery` | **``** (the dense "D" look) | `Table` | | `AdminDatePicker` (react-datepicker) | `` — `cs` locale, `dd.MM.yyyy` | MUI X Pickers (free) | | `.admin-btn` | `