Files
app/docs/superpowers/plans/2026-06-06-mui-migration-phase-3-kit-expansion.md
BOHA 5855c94b50 docs(plan): MUI migration Phase 3 (kit expansion)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:20:01 +02:00

13 KiB

MUI Migration — Phase 3: Kit Expansion — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development (or direct execution with gates). Steps use checkbox (- [ ]) syntax.

Goal: Build the reusable src/admin/ui/ components the page migrations keep needing, so later phases are fast and consistent. No page is migrated in this phase; the dev-only /ui-kit route is extended to showcase + visually verify each new component.

Why these (scout reuse counts): Select ~34 pages, Tabs ~41 files, StatusChip ~34, plus paginated/sortable lists, dates, checkboxes, inline alerts.

Tech Stack: MUI v7 (@mui/material) + @mui/x-date-pickers v8 + date-fns (cs locale) + Emotion. TypeScript strict.

Gates: npx tsc -b --noEmit, npm run build, npx vitest run (148). Visual check (/ui-kit) is the user's. No dev server started by implementers.


File Structure

File Component(s) API
src/admin/ui/Select.tsx Select { value, onChange(v), options?: {value,label}[], children?, ...TextFieldProps } over <TextField select> — composes inside Field
src/admin/ui/StatusChip.tsx StatusChip `{ label, color?: "default" "success" "error" "warning" "info", onClick?, size?, ...ChipProps }`
src/admin/ui/Tabs.tsx Tabs, TabPanel Tabs: { value, onChange(v), tabs: {value,label}[] }; TabPanel: { value, current, children }
src/admin/ui/Pagination.tsx Pagination { page, pageCount, onChange(p) } (renders nothing if pageCount<=1)
src/admin/ui/Checkbox.tsx CheckboxField { label, checked, onChange(b) } (FormControlLabel + Checkbox)
src/admin/ui/Alert.tsx Alert { severity, children, onClose? } (inline MUI Alert)
src/admin/ui/DataTable.tsx extend add DataColumn.sortKey?; DataTableProps.{ sortBy?, sortDir?, onSort?(key) }; render TableSortLabel in sortable headers
src/admin/ui/DateField.tsx DateField `{ value: string("yyyy-MM-dd" ""), onChange(v: string), ...}over MUI XDatePicker, format dd.MM.yyyy`
src/admin/ui/MuiProvider.tsx extend wrap children in LocalizationProvider (AdapterDateFns, cs locale) so date pickers work app-wide
src/admin/ui/index.ts extend export all new components
src/admin/pages/UiKit.tsx extend showcase each new component in both themes

Deferred (build when their page is migrated): Autocomplete (warehouse pickers), FilterBar layout helper.


Task 1: Select, StatusChip, CheckboxField, Alert (simple wrappers)

Files: Create the four files; modify index.ts.

  • Step 1: Select.tsx
import MuiTextField, { type TextFieldProps } from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";

export interface SelectOption {
  value: string | number;
  label: string;
}
type Props = Omit<TextFieldProps, "onChange" | "select" | "value"> & {
  value: string | number;
  onChange: (value: string) => void;
  options?: SelectOption[];
};

/** Dropdown styled like the kit TextField; composes inside <Field>. */
export default function Select({
  value,
  onChange,
  options,
  children,
  ...props
}: Props) {
  return (
    <MuiTextField
      select
      size="small"
      variant="outlined"
      fullWidth
      value={value}
      onChange={(e) => onChange(e.target.value)}
      {...props}
    >
      {options
        ? options.map((o) => (
            <MenuItem key={o.value} value={o.value}>
              {o.label}
            </MenuItem>
          ))
        : children}
    </MuiTextField>
  );
}
  • Step 2: StatusChip.tsx
import Chip, { type ChipProps } from "@mui/material/Chip";

type Props = Omit<ChipProps, "color"> & {
  color?: "default" | "success" | "error" | "warning" | "info";
};

/** Semantic status chip. Pass onClick to make it a toggle. */
export default function StatusChip({
  color = "default",
  size = "small",
  onClick,
  sx,
  ...props
}: Props) {
  return (
    <Chip
      color={color}
      size={size}
      onClick={onClick}
      sx={{ fontWeight: 600, ...(onClick ? { cursor: "pointer" } : {}), ...sx }}
      {...props}
    />
  );
}
  • Step 3: Checkbox.tsx
import FormControlLabel from "@mui/material/FormControlLabel";
import MuiCheckbox from "@mui/material/Checkbox";

export function CheckboxField({
  label,
  checked,
  onChange,
}: {
  label: React.ReactNode;
  checked: boolean;
  onChange: (v: boolean) => void;
}) {
  return (
    <FormControlLabel
      control={
        <MuiCheckbox
          checked={checked}
          onChange={(e) => onChange(e.target.checked)}
        />
      }
      label={label}
    />
  );
}

(Import React type or use import type { ReactNode } from "react" and type label: ReactNode.)

  • Step 4: Alert.tsx
import type { ReactNode } from "react";
import MuiAlert from "@mui/material/Alert";

export default function Alert({
  severity = "info",
  children,
  onClose,
}: {
  severity?: "success" | "error" | "warning" | "info";
  children: ReactNode;
  onClose?: () => void;
}) {
  return (
    <MuiAlert severity={severity} onClose={onClose} sx={{ borderRadius: 2 }}>
      {children}
    </MuiAlert>
  );
}
  • Step 5: export from index.ts: Select (+ type SelectOption), StatusChip, { CheckboxField }, Alert.
  • Step 6: Verify tsc -b clean, build:client OK. Commit (feat(mui): kit — Select, StatusChip, CheckboxField, Alert; + trailer).

Task 2: Tabs + TabPanel, Pagination

Files: Create Tabs.tsx, Pagination.tsx; modify index.ts.

  • Step 1: Tabs.tsx
import type { ReactNode } from "react";
import MuiTabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box";

export interface TabDef {
  value: string;
  label: string;
}

export function Tabs({
  value,
  onChange,
  tabs,
}: {
  value: string;
  onChange: (v: string) => void;
  tabs: TabDef[];
}) {
  return (
    <MuiTabs
      value={value}
      onChange={(_, v) => onChange(v)}
      sx={{
        borderBottom: 1,
        borderColor: "divider",
        minHeight: 44,
        "& .MuiTab-root": { textTransform: "none", fontWeight: 600 },
      }}
    >
      {tabs.map((t) => (
        <Tab key={t.value} value={t.value} label={t.label} />
      ))}
    </MuiTabs>
  );
}

export function TabPanel({
  value,
  current,
  children,
}: {
  value: string;
  current: string;
  children: ReactNode;
}) {
  if (value !== current) return null;
  return <Box sx={{ pt: 2.5 }}>{children}</Box>;
}
  • Step 2: Pagination.tsx
import Box from "@mui/material/Box";
import MuiPagination from "@mui/material/Pagination";

export default function Pagination({
  page,
  pageCount,
  onChange,
}: {
  page: number;
  pageCount: number;
  onChange: (p: number) => void;
}) {
  if (pageCount <= 1) return null;
  return (
    <Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
      <MuiPagination
        page={page}
        count={pageCount}
        onChange={(_, p) => onChange(p)}
        color="primary"
        shape="rounded"
      />
    </Box>
  );
}
  • Step 3: export { Tabs, TabPanel } (+ type TabDef) and Pagination from index.ts.
  • Step 4: Verify tsc -b clean, build:client OK. Commit (feat(mui): kit — Tabs/TabPanel + Pagination; + trailer).

Task 3: Sortable DataTable

Files: Modify src/admin/ui/DataTable.tsx.

  • Step 1: Extend the types:
    • DataColumn<T>: add sortKey?: string;
    • DataTableProps<T>: add sortBy?: string; sortDir?: "asc" | "desc"; onSort?: (key: string) => void;
  • Step 2: In the header cell render, if c.sortKey && onSort is set, wrap the header in TableSortLabel:
import TableSortLabel from "@mui/material/TableSortLabel";
// ...header cell:
{
  c.sortKey && onSort ? (
    <TableSortLabel
      active={sortBy === c.sortKey}
      direction={sortBy === c.sortKey ? (sortDir ?? "asc") : "asc"}
      onClick={() => onSort(c.sortKey!)}
    >
      {c.header}
    </TableSortLabel>
  ) : (
    c.header
  );
}

Non-sortable columns (no sortKey) render the plain header (unchanged). The existing bold/mono/align/rowInactive/empty behavior is untouched.

  • Step 3: Verify tsc -b clean, build:client OK, vitest run (148). Commit (feat(mui): kit — sortable DataTable headers (TableSortLabel); + trailer).

Task 4: DateField + LocalizationProvider

Files: Create src/admin/ui/DateField.tsx; modify MuiProvider.tsx, index.ts.

  • Step 1: Add LocalizationProvider to MuiProvider.tsx wrapping its children, so date pickers work everywhere:
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
import { cs } from "date-fns/locale";
// inside MuiProvider, wrap {children} (and the sync) :
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={cs}>
  <MuiColorSchemeSync />
  {children}
</LocalizationProvider>;

Verify the exact adapter import path for the installed @mui/x-date-pickers@8 against its docs (it may be @mui/x-date-pickers/AdapterDateFns rather than ...V3). Pick the one that resolves; report which.

  • Step 2: DateField.tsx — string-in / string-out (yyyy-MM-dd) to match existing form state:
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { parseISO, format, isValid } from "date-fns";

export default function DateField({
  value,
  onChange,
  ...props
}: { value: string; onChange: (v: string) => void } & Record<string, unknown>) {
  const dateValue = value ? parseISO(value) : null;
  return (
    <DatePicker
      value={dateValue && isValid(dateValue) ? dateValue : null}
      onChange={(d) => onChange(d && isValid(d) ? format(d, "yyyy-MM-dd") : "")}
      format="dd.MM.yyyy"
      slotProps={{ textField: { size: "small", fullWidth: true } }}
      {...props}
    />
  );
}
  • Step 3: export DateField from index.ts.
  • Step 4: Verify tsc -b clean, build:client OK. Commit (feat(mui): kit — DateField over MUI X + cs LocalizationProvider; + trailer).

Task 5: Extend /ui-kit showcase + final review

Files: Modify src/admin/pages/UiKit.tsx.

  • Step 1: Add a section rendering each new component with sample data, in the existing ScopedCssBaseline page: a Select (3 options), StatusChips (each color), Tabs+TabPanel (2 tabs), Pagination (page 2 of 5), CheckboxField, Alert (one of each severity), a sortable DataTable (3 columns, one sortKey, local sort state), and a DateField.
  • Step 2: Verify tsc -b clean, npm run build (server+client) OK, npx vitest run (148). Commit (feat(mui): showcase new kit components in /ui-kit; + trailer).
  • Step 3: Final review of all new components (parallel/adversarial), then finishing-a-development-branch after the user's /ui-kit visual check.

Self-review notes

  • All new components compose with the existing Field (Select/DateField are inputs; Field provides the label) and follow the size=small/outlined/fullWidth defaults.
  • DataTable sort is additive — existing callers (Vehicles) are unaffected (no sortKey/onSort → plain headers).
  • DateField is string-in/string-out so it drops into existing form state (start_date: ""), and uses cs locale + dd.MM.yyyy (matching the date convention in CLAUDE.md).
  • Deferred: Autocomplete, FilterBar — built when warehouse/list pages are migrated.