Files
app/docs/superpowers/plans/2026-06-06-mui-migration-phase-0-foundation.md
2026-06-06 18:54:32 +02:00

21 KiB
Raw Blame History

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):

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:

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:

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
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:

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:

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:

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:

npx vitest run src/admin/theme.test.ts

Expected: PASS (5 tests).

  • Step 5: Commit
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):

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:

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):

  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:

npx vitest run src/admin/theme.test.ts

Expected: PASS (6 tests).

  • Step 5: Commit
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:

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:

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 (
    <ThemeProvider theme={theme} defaultMode="dark">
      <MuiColorSchemeSync />
      {children}
    </ThemeProvider>
  );
}
  • 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):

import MuiProvider from "./ui/MuiProvider";

Then wrap the existing returned tree. Change:

  return (
    <AuthProvider>

to:

  return (
    <MuiProvider>
      <AuthProvider>

and change the matching closing tag at the end of the JSX:

      </AuthProvider>
  );

to:

      </AuthProvider>
    </MuiProvider>
  );
  • Step 4: Verify the app builds and renders with no console errors

Run:

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
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:

import MuiButton, { type ButtonProps } from "@mui/material/Button";

/** App Button: defaults to the brand primary contained style. */
export default function Button(props: ButtonProps) {
  return <MuiButton variant="contained" color="primary" {...props} />;
}

Create src/admin/ui/Card.tsx:

import MuiCard, { type CardProps } from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";

export default function Card({ children, ...props }: CardProps) {
  return (
    <MuiCard {...props}>
      <CardContent>{children}</CardContent>
    </MuiCard>
  );
}

Create src/admin/ui/TextField.tsx:

import MuiTextField, { type TextFieldProps } from "@mui/material/TextField";

/** App TextField: small + outlined + full width by default. */
export default function TextField(props: TextFieldProps) {
  return <MuiTextField size="small" variant="outlined" fullWidth {...props} />;
}
  • Step 2: Create the barrel

Create src/admin/ui/index.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:

npx tsc --noEmit -p tsconfig.json

Expected: PASS.

  • Step 4: Commit
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:

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 (
    <ScopedCssBaseline>
      <Box sx={{ p: 4, minHeight: "100vh", bgcolor: "background.default" }}>
        <Stack direction="row" alignItems="center" spacing={2} mb={3}>
          <Typography variant="h4">UI Kit</Typography>
          <Button color="inherit" variant="outlined" onClick={toggleTheme}>
            Theme: {theme}
          </Button>
        </Stack>

        <Stack spacing={3} sx={{ maxWidth: 520 }}>
          <Card>
            <Typography variant="h6" gutterBottom>
              Faktury
            </Typography>
            <Typography color="text.secondary" gutterBottom>
              12 vystavených tento měsíc
            </Typography>
            <Stack direction="row" spacing={1} mb={2}>
              <Chip label="Zaplaceno" color="success" size="small" />
              <Chip label="Po splatnosti" color="error" size="small" />
            </Stack>
            <TextField
              label="Hledat fakturu"
              placeholder="Číslo nebo klient…"
            />
            <Box mt={2}>
              <Button>Nová faktura</Button>
            </Box>
          </Card>
        </Stack>
      </Box>
    </ScopedCssBaseline>
  );
}
  • 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):

const UiKit = lazy(() => import("./pages/UiKit"));

Then, inside <Routes>, add a sibling route to login (so the showcase renders without the shell), guarded so it is dropped from production builds:

{
  import.meta.env.DEV && <Route path="ui-kit" element={<UiKit />} />;
}
  • Step 3: Verify build (prod) excludes it and type-check passes

Run:

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
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:

--bg-primary: #f5f4f2;

to:

--bg-primary: #f4f3f1;
  • Step 2: Verify build

Run:

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
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:

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:

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 23), provider + ScopedCssBaseline + ThemeContextsetMode 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.