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 -bclean,build:clientOK. 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) andPaginationfromindex.ts. - Step 4: Verify
tsc -bclean,build:clientOK. 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>: addsortKey?: string;DataTableProps<T>: addsortBy?: string; sortDir?: "asc" | "desc"; onSort?: (key: string) => void;
- Step 2: In the header cell render, if
c.sortKey && onSortis set, wrap the header inTableSortLabel:
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 -bclean,build:clientOK,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
LocalizationProvidertoMuiProvider.tsxwrapping 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@8against its docs (it may be@mui/x-date-pickers/AdapterDateFnsrather 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
DateFieldfromindex.ts. - Step 4: Verify
tsc -bclean,build:clientOK. 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
ScopedCssBaselinepage: aSelect(3 options),StatusChips (each color),Tabs+TabPanel(2 tabs),Pagination(page 2 of 5),CheckboxField,Alert(one of each severity), a sortableDataTable(3 columns, onesortKey, local sort state), and aDateField. - Step 2: Verify
tsc -bclean,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-kitvisual 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. DataTablesort is additive — existing callers (Vehicles) are unaffected (nosortKey/onSort→ plain headers).DateFieldis 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.