diff --git a/docs/superpowers/plans/2026-06-06-mui-migration-phase-0-foundation.md b/docs/superpowers/plans/2026-06-06-mui-migration-phase-0-foundation.md new file mode 100644 index 0000000..1eea3f7 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-mui-migration-phase-0-foundation.md @@ -0,0 +1,680 @@ +# MUI Migration — Phase 0: Theming Foundation — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Stand up the MUI theming foundation — install MUI, build `theme.ts` (light+dark from existing tokens), wire the provider + `data-theme` color-scheme bridge + `ThemeContext` sync, apply the token-level palette refresh app-wide, and ship a dev-only `/ui-kit` route proving the theme on the first wrappers (`Button`/`Card`/`TextField`). + +**Architecture:** MUI's CSS-variable theme is configured with `colorSchemeSelector: "[data-theme='%s']"` so it reads the same `data-theme` attribute `ThemeContext` already writes — one toggle drives both MUI and legacy CSS. A tiny sync component mirrors the attribute into MUI's JS color-scheme state. `ScopedCssBaseline` scopes MUI's reset to migrated subtrees (here, only `/ui-kit`); legacy pages keep `base.css`. + +**Tech Stack:** React 18.3, Vite 8, TypeScript (strict), MUI v7 (`@mui/material`) + Emotion + MUI X v8 pickers, Vitest (node). + +**Spec:** `docs/superpowers/specs/2026-06-06-css-mui-migration-design.md` (§3, §4, §7 token-level layer, §8 Phase 0). + +**Scope note:** This plan is Phase 0 ONLY. The rest of the `ui/` kit (Modal, Toast, Select, Switch, Tabs, DataTable, DatePicker), the AppShell (Phase 1), and the Vozidla pilot (Phase 2) are separate plans. + +--- + +## File Structure + +| File | Responsibility | Action | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ------ | +| `package.json` | MUI/Emotion/X-pickers deps; `@types/react` reconciled to 18 | Modify | +| `src/admin/theme.ts` | The MUI theme: cssVariables + colorSchemeSelector, light/dark palettes, typography, shape, component overrides | Create | +| `src/admin/theme.test.ts` | Unit test asserting theme palette/typography/shape values | Create | +| `src/admin/ui/MuiProvider.tsx` | Wraps children in MUI `ThemeProvider` + renders the sync component | Create | +| `src/admin/ui/MuiColorSchemeSync.tsx` | Mirrors `useTheme()` (custom) → MUI `useColorScheme().setMode` | Create | +| `src/admin/ui/Button.tsx`, `Card.tsx`, `TextField.tsx` | Thin brand wrappers over MUI (stable app-facing API) | Create | +| `src/admin/ui/index.ts` | Barrel export for the kit | Create | +| `src/admin/pages/UiKit.tsx` | Dev-only showcase, wrapped in `ScopedCssBaseline` | Create | +| `src/admin/AdminApp.tsx` | Mount `MuiProvider`; add DEV-only `/ui-kit` route | Modify | +| `src/admin/variables.css` | Token-level palette refresh (canvas, chip tints) — app-wide | Modify | + +--- + +## Task 1: Install dependencies and reconcile React types + +**Files:** + +- Modify: `package.json` (via npm) + +- [ ] **Step 1: Install MUI + Emotion + X pickers** + +Run (date-fns ^4 is already a dependency): + +```bash +npm install @mui/material@^7 @emotion/react@^11 @emotion/styled@^11 @mui/x-date-pickers@^8 +``` + +Expected: packages added to `dependencies`; no peer-dependency errors that block install. + +- [ ] **Step 2: Reconcile `@types/react` to the React 18 runtime** + +The repo runs `react@^18.3.1` but pins `@types/react@^19`. MUI's component types are sensitive to the `@types/react` major. Align the types to the runtime: + +```bash +npm install -D @types/react@^18.3 @types/react-dom@^18.3 +``` + +Expected: devDependencies now show `@types/react@^18.3.x`. + +- [ ] **Step 3: Verify the project still type-checks and builds** + +Run: + +```bash +npx tsc --noEmit -p tsconfig.json +npm run build:client +``` + +Expected: both succeed with no errors (this is the pre-change baseline; if `tsc` was already failing, capture that before proceeding). + +- [ ] **Step 4: Commit** + +```bash +git add package.json package-lock.json +git commit -m "build(mui): add MUI v7 + Emotion + X pickers; align @types/react to 18" +``` + +--- + +## Task 2: Create `theme.ts` with light/dark palettes (TDD) + +**Files:** + +- Create: `src/admin/theme.ts` +- Test: `src/admin/theme.test.ts` + +Token values are taken from `src/admin/variables.css` (`[data-theme="light"]` / `[data-theme="dark"]`). + +- [ ] **Step 1: Write the failing test** + +Create `src/admin/theme.test.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { theme } from "./theme"; + +describe("MUI theme", () => { + it("uses the brand fonts", () => { + expect(theme.typography.fontFamily).toContain("Plus Jakarta Sans"); + expect(String(theme.typography.h1.fontFamily)).toContain("Urbanist"); + }); + + it("maps the brand red per color scheme", () => { + expect(theme.colorSchemes.light.palette.primary.main).toBe("#c73030"); + expect(theme.colorSchemes.dark.palette.primary.main).toBe("#d63031"); + }); + + it("maps the refreshed light canvas + paper", () => { + expect(theme.colorSchemes.light.palette.background.default).toBe("#f4f3f1"); + expect(theme.colorSchemes.light.palette.background.paper).toBe("#ffffff"); + }); + + it("maps semantic colors per scheme", () => { + expect(theme.colorSchemes.light.palette.error.main).toBe("#b91c1c"); + expect(theme.colorSchemes.dark.palette.success.main).toBe("#22c55e"); + }); + + it("uses the base radius of 10", () => { + expect(theme.shape.borderRadius).toBe(10); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: + +```bash +npx vitest run src/admin/theme.test.ts +``` + +Expected: FAIL — `Cannot find module './theme'` (or `theme` is undefined). + +- [ ] **Step 3: Implement `theme.ts` (palettes/typography/shape only — overrides come in Task 3)** + +Create `src/admin/theme.ts`: + +```ts +import { createTheme } from "@mui/material/styles"; + +const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif"; +const FONT_HEADING = "'Urbanist', sans-serif"; +const FONT_MONO = "'DM Mono', Menlo, monospace"; + +export const theme = createTheme({ + cssVariables: { + // Read the SAME attribute ThemeContext already writes (`data-theme`). + // The "%s" placeholder is required; value must start with "." or "[". + colorSchemeSelector: "[data-theme='%s']", + }, + colorSchemes: { + light: { + palette: { + mode: "light", + primary: { main: "#c73030" }, + success: { main: "#15803d" }, + warning: { main: "#b45309" }, + error: { main: "#b91c1c" }, + info: { main: "#1d4ed8" }, + background: { default: "#f4f3f1", paper: "#ffffff" }, + text: { primary: "#1a1a1a", secondary: "#555555" }, + divider: "rgba(0,0,0,0.1)", + }, + }, + dark: { + palette: { + mode: "dark", + primary: { main: "#d63031" }, + success: { main: "#22c55e" }, + warning: { main: "#f59e0b" }, + error: { main: "#ef4444" }, + info: { main: "#3b82f6" }, + 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 }, + h5: { fontFamily: FONT_HEADING, fontWeight: 700 }, + h6: { fontFamily: FONT_HEADING, fontWeight: 700 }, + button: { textTransform: "none", fontWeight: 600 }, + }, +}); + +// Re-exported for tasks/components that need the mono stack. +export const fonts = { + body: FONT_BODY, + heading: FONT_HEADING, + mono: FONT_MONO, +}; +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: + +```bash +npx vitest run src/admin/theme.test.ts +``` + +Expected: PASS (5 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/admin/theme.ts src/admin/theme.test.ts +git commit -m "feat(mui): theme.ts with light/dark palettes, brand fonts, data-theme selector" +``` + +--- + +## Task 3: Add component overrides (the "Soft-SaaS" defaults) + +**Files:** + +- Modify: `src/admin/theme.ts` +- Test: `src/admin/theme.test.ts` + +- [ ] **Step 1: Add a failing assertion for the overrides** + +Append to `src/admin/theme.test.ts` (inside the `describe`): + +```ts +it("makes buttons pill-shaped and cards 16px by default", () => { + const btn = theme.components?.MuiButton?.styleOverrides?.root as any; + expect(btn?.borderRadius).toBe(999); + const card = theme.components?.MuiCard?.styleOverrides?.root as any; + expect(card?.borderRadius).toBe(16); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: + +```bash +npx vitest run src/admin/theme.test.ts +``` + +Expected: FAIL — `theme.components` is undefined / `borderRadius` is undefined. + +- [ ] **Step 3: Add `components` to the `createTheme` call** + +In `src/admin/theme.ts`, add a `components` key to the `createTheme({...})` object (after `typography`): + +```ts + components: { + MuiButton: { + defaultProps: { disableElevation: true }, + styleOverrides: { + root: { borderRadius: 999, paddingInline: "0.95rem" }, + containedPrimary: { + backgroundImage: "linear-gradient(135deg, #e23a3a, #c01f1f)", + boxShadow: "0 5px 14px rgba(214,48,49,0.32)", + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 16, + boxShadow: + "0 6px 20px rgba(20,20,40,0.06), 0 1px 2px rgba(0,0,0,0.03)", + }, + }, + }, + MuiChip: { + styleOverrides: { root: { borderRadius: 999, fontWeight: 700 } }, + }, + }, +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: + +```bash +npx vitest run src/admin/theme.test.ts +``` + +Expected: PASS (6 tests). + +- [ ] **Step 5: Commit** + +```bash +git add src/admin/theme.ts src/admin/theme.test.ts +git commit -m "feat(mui): soft-SaaS component defaults (pill buttons, 16px cards, pill chips)" +``` + +--- + +## Task 4: MUI provider + color-scheme sync + +**Files:** + +- Create: `src/admin/ui/MuiColorSchemeSync.tsx` +- Create: `src/admin/ui/MuiProvider.tsx` +- Modify: `src/admin/AdminApp.tsx` + +- [ ] **Step 1: Create the sync component** + +Create `src/admin/ui/MuiColorSchemeSync.tsx`: + +```tsx +import { useEffect } from "react"; +import { useColorScheme } from "@mui/material/styles"; +import { useTheme as useAppTheme } from "../../context/ThemeContext"; + +/** + * ThemeContext is the single owner of the `data-theme` attribute. This mirrors + * its value into MUI's JS color-scheme state so theme.applyStyles('dark') and + * useColorScheme().mode stay in sync with the attribute the CSS selector reads. + */ +export default function MuiColorSchemeSync() { + const { theme } = useAppTheme(); + const { mode, setMode } = useColorScheme(); + + useEffect(() => { + if (mode !== theme) setMode(theme as "light" | "dark"); + }, [theme, mode, setMode]); + + return null; +} +``` + +- [ ] **Step 2: Create the provider wrapper** + +Create `src/admin/ui/MuiProvider.tsx`: + +```tsx +import type { ReactNode } from "react"; +import { ThemeProvider } from "@mui/material/styles"; +import { theme } from "../theme"; +import MuiColorSchemeSync from "./MuiColorSchemeSync"; + +export default function MuiProvider({ children }: { children: ReactNode }) { + return ( + + + {children} + + ); +} +``` + +- [ ] **Step 3: Mount the provider at the top of `AdminApp`** + +In `src/admin/AdminApp.tsx`, add the import after the other component imports (e.g. after line 10): + +```tsx +import MuiProvider from "./ui/MuiProvider"; +``` + +Then wrap the existing returned tree. Change: + +```tsx + return ( + +``` + +to: + +```tsx + return ( + + +``` + +and change the matching closing tag at the end of the JSX: + +```tsx + + ); +``` + +to: + +```tsx + + + ); +``` + +- [ ] **Step 4: Verify the app builds and renders with no console errors** + +Run: + +```bash +npx tsc --noEmit -p tsconfig.json +npm run build:client +``` + +Expected: both succeed. (The user runs the dev server separately — do not start it. Verification of live render happens via the `/ui-kit` route in Task 6.) + +- [ ] **Step 5: Commit** + +```bash +git add src/admin/ui/MuiProvider.tsx src/admin/ui/MuiColorSchemeSync.tsx src/admin/AdminApp.tsx +git commit -m "feat(mui): mount MUI provider + sync color scheme to data-theme" +``` + +--- + +## Task 5: First UI-kit wrappers + barrel + +**Files:** + +- Create: `src/admin/ui/Button.tsx`, `src/admin/ui/Card.tsx`, `src/admin/ui/TextField.tsx`, `src/admin/ui/index.ts` + +- [ ] **Step 1: Create the wrappers** + +Create `src/admin/ui/Button.tsx`: + +```tsx +import MuiButton, { type ButtonProps } from "@mui/material/Button"; + +/** App Button: defaults to the brand primary contained style. */ +export default function Button(props: ButtonProps) { + return ; +} +``` + +Create `src/admin/ui/Card.tsx`: + +```tsx +import MuiCard, { type CardProps } from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; + +export default function Card({ children, ...props }: CardProps) { + return ( + + {children} + + ); +} +``` + +Create `src/admin/ui/TextField.tsx`: + +```tsx +import MuiTextField, { type TextFieldProps } from "@mui/material/TextField"; + +/** App TextField: small + outlined + full width by default. */ +export default function TextField(props: TextFieldProps) { + return ; +} +``` + +- [ ] **Step 2: Create the barrel** + +Create `src/admin/ui/index.ts`: + +```ts +export { default as MuiProvider } from "./MuiProvider"; +export { default as Button } from "./Button"; +export { default as Card } from "./Card"; +export { default as TextField } from "./TextField"; +``` + +- [ ] **Step 3: Verify type-check** + +Run: + +```bash +npx tsc --noEmit -p tsconfig.json +``` + +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src/admin/ui/Button.tsx src/admin/ui/Card.tsx src/admin/ui/TextField.tsx src/admin/ui/index.ts +git commit -m "feat(mui): first ui-kit wrappers (Button, Card, TextField) + barrel" +``` + +--- + +## Task 6: Dev-only `/ui-kit` showcase route + +**Files:** + +- Create: `src/admin/pages/UiKit.tsx` +- Modify: `src/admin/AdminApp.tsx` + +- [ ] **Step 1: Create the showcase page** + +Create `src/admin/pages/UiKit.tsx`: + +```tsx +import ScopedCssBaseline from "@mui/material/ScopedCssBaseline"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Chip from "@mui/material/Chip"; +import { Button, Card, TextField } from "../ui"; +import { useTheme } from "../../context/ThemeContext"; + +export default function UiKit() { + const { theme, toggleTheme } = useTheme(); + return ( + + + + UI Kit + + + + + + + Faktury + + + 12 vystavených tento měsíc + + + + + + + + + + + + + + ); +} +``` + +- [ ] **Step 2: Register the route, DEV-only** + +In `src/admin/AdminApp.tsx`, add the lazy import alongside the other lazy pages (after line 11's eager imports / with the `lazy(...)` block): + +```tsx +const UiKit = lazy(() => import("./pages/UiKit")); +``` + +Then, inside ``, add a sibling route to `login` (so the showcase renders without the shell), guarded so it is dropped from production builds: + +```tsx +{ + import.meta.env.DEV && } />; +} +``` + +- [ ] **Step 3: Verify build (prod) excludes it and type-check passes** + +Run: + +```bash +npx tsc --noEmit -p tsconfig.json +npm run build:client +``` + +Expected: both succeed. `import.meta.env.DEV` is `false` in the production build, so the route is tree-shaken. + +- [ ] **Step 4: Manual + Playwright verification** + +The user runs the dev server. Once it's up, verify at `/ui-kit`: + +- Card, button (red gradient, pill), chips (tinted), and text field render in the Soft-SaaS style. +- Clicking **Theme: dark/light** flips `data-theme` and **both MUI components AND the page background** swap — proving the `colorSchemeSelector` bridge works. +- No new console errors. + +Capture baseline screenshots with the available Playwright tooling (light + dark) and save under `docs/superpowers/visual-baselines/phase-0-ui-kit-{light,dark}.png`. + +- [ ] **Step 5: Commit** + +```bash +git add src/admin/pages/UiKit.tsx src/admin/AdminApp.tsx +git commit -m "feat(mui): dev-only /ui-kit showcase proving the theme + data-theme bridge" +``` + +--- + +## Task 7: Token-level palette refresh (app-wide) + +**Files:** + +- Modify: `src/admin/variables.css` + +Per spec §7, the token-level palette refresh is applied app-wide so migrated and legacy pages share the refreshed palette. The only token change for Phase 0 is the light canvas (the new look's `#f4f3f1` vs current `#f5f4f2`); chip/structure changes are component-level (MUI) and need no token edit. + +- [ ] **Step 1: Update the light canvas token** + +In `src/admin/variables.css`, inside the `[data-theme="light"]` block, change: + +```css +--bg-primary: #f5f4f2; +``` + +to: + +```css +--bg-primary: #f4f3f1; +``` + +- [ ] **Step 2: Verify build** + +Run: + +```bash +npm run build:client +``` + +Expected: succeeds. + +- [ ] **Step 3: Manual check** + +With the dev server (user-run), confirm in light mode the app canvas is the refreshed `#f4f3f1` and nothing else shifted structurally. (This is the deliberate, minor app-wide color change noted in the spec — not a no-op.) + +- [ ] **Step 4: Commit** + +```bash +git add src/admin/variables.css +git commit -m "style(tokens): refresh light canvas to #f4f3f1 (two-layer refresh, token level)" +``` + +--- + +## Task 8: Phase-0 release verification + +**Files:** none (verification + tag) + +- [ ] **Step 1: Full type-check, unit tests, and build** + +Run: + +```bash +npx tsc --noEmit -p tsconfig.json +npx vitest run src/admin/theme.test.ts +npm run build +``` + +Expected: `tsc` clean; 6 theme tests pass; server + client build succeed. + +- [ ] **Step 2: Backend test suite still green (server untouched)** + +Run: + +```bash +npm test +``` + +Expected: existing `auth` + `numbering` suites pass (Phase 0 changed no server code). + +- [ ] **Step 3: Definition-of-done checklist (manual, dev server)** + +- `/ui-kit` renders Button/Card/TextField/Chip in the Soft-SaaS style in **both** themes. +- Theme toggle flips `data-theme` and swaps MUI + legacy colors together; MUI's `useColorScheme().mode` matches (no console warnings). +- No new console errors on any existing page (the provider mount + token refresh are non-breaking). +- Light canvas is `#f4f3f1`. +- Production build excludes the `/ui-kit` route. + +- [ ] **Step 4: Tag/release per the project release process** + +Follow `CLAUDE.md` "Release Process" (bump `package.json`, build, tag, deploy) when shipping Phase 0. No Prisma migration is involved (frontend-only). + +--- + +## Self-review notes (for the controller) + +- **Spec coverage:** Phase 0 items from spec §8 are all covered — deps + `@types/react` (Task 1), `theme.ts` light/dark (Tasks 2–3), provider + `ScopedCssBaseline` + `ThemeContext`↔`setMode` sync (Tasks 4, 6), dev-only `/ui-kit` (Task 6), token-level refresh (Task 7). The full `ui/` kit, AppShell, and Vozidla are explicitly deferred to later plans (scope note). +- **Type consistency:** `theme` (named export) is used consistently in `theme.test.ts`, `MuiProvider.tsx`. `useTheme` refers to the app's `ThemeContext` (aliased `useAppTheme` in the sync component to avoid colliding with MUI's `useTheme`). Wrappers are default exports re-exported via the barrel. +- **Open risk to verify in Task 1/6:** exact MUI v7 API for `cssVariables.colorSchemeSelector` + `ThemeProvider` `defaultMode` — confirm against the installed version's docs; if `defaultMode` belongs on a different provider in the installed version, adjust in Task 4.