docs(spec): CSS->MUI migration design (soft-SaaS + dense tables, incremental)

Approved design for migrating the admin frontend off ~7,100 lines of hand-written CSS onto MUI, themed to the existing brand, dark/light preserved, modern 'soft-SaaS shell + dense tables' look, rolled out incrementally (foundation-first, always shippable). Verified via 4-way adversarial review (editorial, MUI-docs accuracy, codebase-fit, completeness) with fixes applied. Also gitignores .superpowers/ brainstorm mockups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 18:44:17 +02:00
parent 596d21712a
commit a8c0e413f4
2 changed files with 254 additions and 0 deletions

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ dist-client/
# Claude worktrees
.claude/worktrees/
.claude/settings.local.json
# Superpowers brainstorm mockups
.superpowers/

View File

@@ -0,0 +1,251 @@
# 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 | A focused CRUD page (Vozidla _or_ Uživatelé) |
| 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 `h1h6`, 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 `<html>`. 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 (`<html>`, 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` | `<Modal>` — preserves **FormModal's** full prop set (`submitDisabled`, `subtitle`, `loading`); ConfirmModal needs only `title`/`message`/`type`/`confirmVariant`/`loading` | `Dialog` |
| `.admin-table*` + `useTableSort` + `usePaginatedQuery` | **`<DataTable>`** (the dense "D" look) | `Table` |
| `AdminDatePicker` (react-datepicker) | `<DatePicker>` — `cs` locale, `dd.MM.yyyy` | MUI X Pickers (free) |
| `.admin-btn` | `<Button>` (pill / red-gradient default) | `Button` |
| `.admin-form-*` inputs/selects | `<TextField>` / `<Select>` / `<Switch>` | MUI form controls |
| `AlertContext` toasts | `<Toast>` — keeps `useAlert()` API | `Snackbar` |
| `AdminLayout` sidebar/topbar | `<AppShell>` — the soft-SaaS shell | `Box` / `Drawer` |
| KPI tiles | `<KpiCard>` / `<Card>` | `Card` |
| Tabs | `<Tabs>` | `Tabs` |
**Modal behavior change (deliberate a11y gain — not a 1:1 match).** Today's modals use `useModalLock` (a ref-counted body-scroll lock, used in **8 places**, including non-`FormModal` spots like Dashboard, OfferDetail, ReceivedInvoices, WarehouseItemDetail) plus `role="dialog"`, but have **no focus trap, no initial-focus, no return-focus**. MUI `Dialog` adds scroll-lock _and_ a focus trap + return-focus. This is an intentional improvement; it is called out so it isn't a surprise. **Audit all 8 `useModalLock` call sites** — those not backed by `FormModal`/`ConfirmModal` either migrate to `<Modal>` or keep `useModalLock` until their page migrates. Verify stacked/nested modal scroll-lock still works.
### 5.1 Dense `<DataTable>` (centerpiece)
MUI `Table` styled to the compact "D" look — uppercase micro headers, **DM Mono right-aligned numerics**, tinted status `Chip`s, subtle row hover — wired to the **existing `useTableSort` (column sort) and `usePaginatedQuery` (list + pagination) hooks**. The current `.admin-table-responsive` horizontal-scroll wrapper carries over as MUI's `TableContainer`.
- **Numbers stay formatted by the existing utilities.** Numeric/currency cells continue to render through `src/admin/utils/formatters.ts` (`formatCurrency` cs-CZ + ``/``, `formatKm`, `czechPlural`). `<DataTable>` only _styles_ (DM Mono, right-align); it never re-formats values.
- A mobile card-view variant for the densest tables is a future enhancement (out of scope for the first pass).
### 5.2 Date pickers
Swap `AdminDatePicker` → MUI X Date Pickers (free **Community** tier; only Range Pickers are Pro, which we don't need) with the `date-fns` adapter and **`cs` locale**. `AdminDatePicker` is used in **three variants**, all covered by Community:
| Current (react-datepicker) | → MUI X | Format (date-fns tokens) |
| -------------------------- | ---------------------------------- | ------------------------ |
| date | `DatePicker` | `dd.MM.yyyy` |
| `showMonthYearPicker` | `DatePicker` with month/year views | `MM/yyyy` |
| `showTimeSelectOnly` | `TimePicker` | `HH:mm` |
> **Format-token correction:** date-fns tokens are case-sensitive — `MM` = month, `mm` = minutes, `yyyy` = calendar year (`rrrr` = local-week-year, wrong). The format is **`dd.MM.yyyy`**, not `dd.mm.rrrr`. Apply via `LocalizationProvider` `dateFormats` (e.g. `{ keyboardDate: "dd.MM.yyyy" }`).
Retires a custom component and the `react-datepicker` dependency; MUI's responsive picker handles touch/mobile.
---
## 6. CSS end-state (explicit)
1. **Gone — ~90% of files (~80% of lines).** All per-feature CSS files deleted as their pages migrate: `buttons`, `forms`, `tables`, `invoices`, `offers`, `attendance`, `dashboard`, `warehouse`, `login`, `settings`, `filemanager`, `pagination`, `datepicker`, `layout`, `base`, `components`, **`responsive`** (the 6-line placeholder). What they expressed becomes the MUI theme + `sx`/`styled`.
2. **Tokens move, don't die.** `variables.css` is rewritten into `theme.ts` (TypeScript). With MUI's CSS-variable mode, MUI emits those tokens as CSS custom properties at runtime — so the hand-written `variables.css` file is retired, but the values live on in TS as the single source.
3. **Stays — a small residue** for the genuinely-bespoke widgets MUI does not provide:
- **Plan/gantt grid** (`plan.css`, ~1,152 lines, slimmed) — kept as a custom component with a trimmed stylesheet (largest survivor; ~16% of today's lines, hence "~80% of lines removed" not 90%).
- **Quill rich-text editor** and **Leaflet map** — kept, with small theming CSS.
- These surviving stylesheets reference the **MUI-emitted CSS variables**, so they stay in sync with the theme and dark/light.
**Stated precisely:** custom React components stay (their logic is the value); standard ones are re-skinned via MUI and lose their custom CSS; styling comes from the MUI theme; the only custom CSS remaining is the slim stylesheets for the 3 bespoke widgets, which still read the MUI theme tokens. Literally-zero custom CSS would require rebuilding the plan grid from MUI primitives — explicitly **out of scope** (YAGNI).
---
## 7. Aesthetic spec ("Soft-SaaS + dense", validated 2026-06-06)
The refresh is delivered in **two layers**, which is how it reconciles with the "shared palette" coexistence model:
- **Token-level (palette) — applies app-wide, early.** These are small refinements to the existing tokens (e.g. light canvas `#f5f4f2#f4f3f1`, refined status-chip tints). They are updated once in `variables.css`/`theme.ts` in Phase 0, so **both** legacy and migrated pages share the refreshed palette. This means Phase 0 _does_ shift colors slightly app-wide — it is **not** visually a no-op — but layout/structure is unchanged.
- **Component-level (the "modern look") — per page, via MUI.** The distinctive new look (red-gradient pill buttons, 16px soft-shadow rounded cards, the soft-SaaS shell, dense tables) is delivered through MUI component styling as each page migrates. Unmigrated pages keep their current structure on the refreshed palette.
So §4's "coexist" promise is **same palette/typography, not pixel-identical structure**.
Concrete values implementers must reproduce (light shown):
- **Canvas:** `#f4f3f1`. **Surfaces/cards:** `#ffffff`, radius **16px**, soft shadow `0 6px 20px rgba(20,20,40,.06), 0 1px 2px rgba(0,0,0,.03)`.
- **Sidebar:** transparent on canvas; active item = white pill with soft shadow + red text/icon.
- **Top bar:** Urbanist 800 page title; pill search field; circular icon buttons; gradient avatar; primary action = red-gradient pill button.
- **Primary button:** `linear-gradient(135deg,#e23a3a,#c01f1f)`, radius `999px`, shadow `0 5px 14px rgba(214,48,49,.32)`.
- **Status chips:** pill, soft tinted — paid `#eafaf0`/`#2b8a3e`, overdue `#fdecec`/`#c92a2a`, draft `#fbf3dd`/`#92760b`.
- **Dense table:** padding ~`.5rem 1rem`; headers `.64rem` uppercase, letter-spacing `.05em`, muted; **DM Mono** right-aligned numeric cells; row hover `#faf9f7`; row borders `#f5f3f1`; container is a 16px soft card.
- **Fonts:** Urbanist 600800 (headings), Plus Jakarta Sans 400700 (body), DM Mono 400500 (figures).
**Dark mode.** Several §7 values are new and have no current dark counterpart, so dark is **authored in `theme.ts` during Phase 0** by deriving from these light values + the existing dark surfaces (canvas `#0a0a0a`/`#0f0f0f`, glass borders `rgba(255,255,255,.08)`, dark chip tints), not literally "mirrored."
**Visual references** (persisted, gitignored): `.superpowers/brainstorm/9839-1780760811/content/` — `aesthetic-directions.html`, `refined-hybrid-faktury.html`.
---
## 8. Migration phasing
Each phase is **one or more** normal `v1.x` releases (realistic total: ~a dozen releases). Each migrated page deletes its own CSS file.
- **Phase 0 — Foundation.** Install MUI v7 + Emotion + MUI X v8 Pickers; reconcile `@types/react` with the React 18 runtime (§14); write `theme.ts` from tokens **and apply the token-level palette refresh app-wide** (§7); author the dark palette; wire `ScopedCssBaseline` + the `ThemeContext`↔`setMode()` sync; build the `src/admin/ui/` kit; add the **dev-only** `/ui-kit` showcase route (guarded by `import.meta.env.DEV`, tree-shaken from prod). Layout/structure unchanged; colors shift slightly app-wide. Deletes no per-page CSS yet.
- **Phase 1 — `<AppShell>` (chrome).** Migrate sidebar/topbar/theme-toggle to the soft-SaaS shell. Every page _inside the AdminLayout route_ sits in the new frame; unmigrated content still uses its own CSS inside it. Retires most of `layout.css`. **Login is NOT touched here** — it is a sibling route outside `AdminLayout` (`AdminApp.tsx:100`) with its own full-screen layout; see Phase 3.
- **Phase 2 — Pilot CRUD page** (Vozidla _or_ Uživatelé). Full end-to-end: list, dense `<DataTable>`, filters, create/edit `<Modal>`. Battle-tests and tunes the foundation. Deletes that page's CSS.
- **Phase 3 — Simple CRUD + Login.** The non-pilot of Users/Vehicles, Projects, Settings, and **Login** (migrated standalone — keeps its own full-screen layout, theme toggle, and 2FA two-step + shake/animate-out flow; deletes `login.css`).
- **Phase 4 — List-heavy** (Offers, Invoices lists). Hammers `<DataTable>`. Invoice/offer **on-screen detail** (Quill, the detail page) re-themed, not rebuilt. (Generated PDFs are out of scope — §13.)
- **Phase 5 — Attendance & Leave/Trips** (grids + forms).
- **Phase 6 — Warehouse, then Plan + Dashboard polish.**
- **Phase 7 — Teardown.** Delete leftover legacy CSS; retire `variables.css` into `theme.ts`; only the slim bespoke-widget CSS remains.
At no point is the app frozen or un-shippable.
---
## 9. Bespoke components & animation
**Bespoke components stay custom, re-themed (their logic is the value):**
- **Plan/gantt grid** (`plan.css`) — keep the grid; repaint from tokens; wrap its controls in the kit.
- **Leaflet maps** — keep; align container/control styling to the theme.
- **Quill rich-text** (`react-quill-new`) — keep; wrap as `<RichTextEditor>`; theme the toolbar/editor. **Server-side DOMPurify + `cleanQuillHtml` sanitization on the PDF and render paths is unaffected and must remain.**
- **@dnd-kit** drag-drop — keep (behavior, not styling).
**Animation (framer-motion).** framer-motion is used in ~40 files, not just modals. Decision: **keep framer-motion** for the animations MUI does not cover — the **AdminLayout logout transition** (scale + blur + fade), **dashboard card entrances**, **Login** transitions, and plan interactions. MUI's own transitions replace framer-motion **only** inside the components that become MUI (`Dialog` for modals, `Snackbar` for toasts). When migrating `AdminLayout`→`<AppShell>` and the dashboard, **consciously re-create or drop** their framer-motion animations (don't lose them silently). `useReducedMotion` continues to gate bespoke animations; MUI honors `prefers-reduced-motion` via its theme transitions.
---
## 10. Testing & verification
This is a UI restyle, so behavior must stay identical (except the deliberate modal a11y gain, §5).
- **Backend/API tests** (`auth`, `numbering`) unaffected; run each phase to confirm the server is untouched — they stay green.
- **Per-page manual verification checklist** (primary gate): every CRUD action, filter, sort, pagination, and modal works as before; **modal focus order / ESC / backdrop-close** behave; **dark and light** both render; **mobile + desktop** (responsive parity, §11); **zero new console errors**; `tsc --noEmit` and `vite build` pass.
- **`/ui-kit` dev route:** renders every foundation component in both themes — instant visual QA when tuning the theme; living reference for page migration. Dev-only (stripped from prod).
- **Playwright smoke + before/after screenshots:** a thin per-page script loads the page, runs the core flow, captures light+dark screenshots to diff against the pre-migration baseline. (Playwright is already available in the environment.)
---
## 11. Risks & mitigations
| Risk | Mitigation |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bundle/runtime cost (Emotion) | Measure after Phase 0; tree-shake; acceptable for an internal admin app |
| MUI reset clashing with legacy `base.css` mid-migration | `ScopedCssBaseline` around migrated subtrees; provider above it; migrate the shell early |
| Dark/light drift between MUI and legacy CSS | One `data-theme` attribute (owned by `ThemeContext`) + `setMode()` sync drives both |
| **Modal behavior change** (focus trap, return-focus) | Intentional a11y gain; audit the 8 `useModalLock` sites; add focus/ESC/backdrop to the per-page checklist |
| **Responsive parity** — 72 media queries live in the CSS files being deleted | Re-express each page's breakpoints via MUI theme breakpoints / responsive `sx` **when that page migrates** — part of per-page DoD; review high-MQ files (`layout`, `components`, `plan`) explicitly |
| **`@types/react` 19 vs React 18.3 skew** | Reconcile types to the React 18 runtime in Phase 0 before building the kit, so the `tsc --noEmit` gate isn't blocked by pre-existing skew |
| Czech dates in MUI X Pickers | `date-fns` `cs` locale, **`dd.MM.yyyy`** / `MM/yyyy` / `HH:mm` — verified in Phase 0 |
| Czech number/currency formatting | Tables keep `formatters.ts` (`formatCurrency`, `formatKm`); DataTable only styles |
| **Per-phase rollback** | Each phase is a self-contained, independently-revertable PR (page migration + its CSS deletion together). Coexistence means reverting one migrated page doesn't affect others. **Phase 1 (AppShell) is the highest-blast-radius phase** — ship behind extra verification; revert = `git revert` the phase PR + redeploy |
| Scope creep over a long migration | Strict phase discipline; **restyle only — never refactor logic in the same PR** |
---
## 12. Definition of done
- **Per page:** rendered via the `ui/` kit; its CSS file deleted; all interactions verified; **its media queries re-expressed via MUI breakpoints/`sx`**; modal focus/ESC/backdrop verified; dark + light + mobile + desktop pass; `tsc` + `vite build` green; no new console errors; shipped.
- **Overall:** every page migrated; all per-feature CSS deleted (incl. `responsive.css`); `variables.css` retired into `theme.ts`; only the slim bespoke-widget CSS remains; the dev-only `/ui-kit` route reflects the full theme (and stays stripped from prod).
---
## 13. Out of scope / deferred
- **Puppeteer PDF templates** (`src/routes/admin/invoices-pdf.ts`, `offers-pdf.ts`, `orders-pdf.ts`) use their **own self-contained inline `<style>` + print fonts**, independent of the admin stylesheet — **NOT** part of this migration and not aligned to the MUI theme. (DOMPurify + `cleanQuillHtml` sanitization on those paths is unaffected.) Phase 4's "detail re-themed" refers to the **on-screen** detail page only.
- Rebuilding the plan/gantt grid from MUI primitives (kept custom-but-themed).
- Mobile card-view table variant (future enhancement).
- Any backend, API, or business-logic change.
- Replacing Leaflet, Quill, or @dnd-kit (kept).
---
## 14. Open items to confirm during planning (Phase 0)
- Confirm **MUI v7** + **MUI X v8** as the pinned versions and re-verify the `cssVariables.colorSchemeSelector` + `colorSchemes` API against the installed version's docs.
- **Reconcile `@types/react`** with the React 18.3 runtime (or deliberately validate the chosen MUI version against React-18-runtime-with-React-19-types) before committing the kit.
- Verify each of the three MUI X picker variants under the `date-fns` `cs` adapter: `DatePicker` (date), `DatePicker` month/year views (replacing `showMonthYearPicker`), `TimePicker` (replacing `showTimeSelectOnly`).
- Final pilot page choice between **Vozidla** and **Uživatelé**.
- Audit the 8 `useModalLock` call sites and decide migrate-now vs keep-until-page-migrates for each.
- Bundle-size budget/target to check against after Phase 0.
> **Note:** `CLAUDE.md` references a hook named `useListData` that does not exist (the real hook is `usePaginatedQuery`); this spec uses the correct name. Correcting `CLAUDE.md` is advisable but outside this spec's scope.