feat(mui): data/form kit — Modal, ConfirmDialog, DataTable, Field/SwitchField

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 20:44:16 +02:00
parent efaf49e9df
commit bd658cd00c
5 changed files with 287 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogActions from "@mui/material/DialogActions";
import MuiButton from "@mui/material/Button";
export interface ConfirmDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message: string;
confirmText?: string;
cancelText?: string;
confirmVariant?: "primary" | "danger";
loading?: boolean;
}
/** Confirmation dialog over MUI Dialog. Preserves the legacy ConfirmModal prop shape. */
export default function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = "Potvrdit",
cancelText = "Zrušit",
confirmVariant = "primary",
loading = false,
}: ConfirmDialogProps) {
return (
<Dialog
open={isOpen}
onClose={loading ? undefined : onClose}
slotProps={{ paper: { sx: { borderRadius: 3 } } }}
>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{message}</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<MuiButton onClick={onClose} color="inherit" disabled={loading}>
{cancelText}
</MuiButton>
<MuiButton
onClick={onConfirm}
variant="contained"
color={confirmVariant === "danger" ? "error" : "primary"}
disabled={loading}
>
{loading ? "Pracuji…" : confirmText}
</MuiButton>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,82 @@
import type { ReactNode } from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
export interface DataColumn<T> {
key: string;
header: string;
align?: "left" | "right";
/** Render the cell with the DM Mono numeric face. */
mono?: boolean;
render: (row: T) => ReactNode;
}
export interface DataTableProps<T> {
columns: DataColumn<T>[];
rows: T[];
rowKey: (row: T) => string | number;
rowInactive?: (row: T) => boolean;
empty?: ReactNode;
}
/** Dense, Soft-SaaS-styled data table. Styling only — never re-formats values. */
export default function DataTable<T>({
columns,
rows,
rowKey,
rowInactive,
empty,
}: DataTableProps<T>) {
if (rows.length === 0 && empty) return <>{empty}</>;
return (
<TableContainer sx={{ borderRadius: 2 }}>
<Table size="small" sx={{ "& td, & th": { borderColor: "divider" } }}>
<TableHead>
<TableRow>
{columns.map((c) => (
<TableCell
key={c.key}
align={c.align}
sx={{
textTransform: "uppercase",
letterSpacing: ".05em",
fontSize: ".64rem",
fontWeight: 700,
color: "text.secondary",
}}
>
{c.header}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<TableRow
key={rowKey(row)}
hover
sx={rowInactive?.(row) ? { opacity: 0.55 } : undefined}
>
{columns.map((c) => (
<TableCell
key={c.key}
align={c.align}
sx={{
fontSize: ".8rem",
...(c.mono ? { fontFamily: "'DM Mono', monospace" } : {}),
}}
>
{c.render(row)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}

75
src/admin/ui/Field.tsx Normal file
View File

@@ -0,0 +1,75 @@
import type { ReactNode } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
/** Labeled field wrapper: label (+required) + the input + an error/hint line. */
export function Field({
label,
required,
error,
hint,
children,
}: {
label: string;
required?: boolean;
error?: string;
hint?: string;
children: ReactNode;
}) {
return (
<Box sx={{ mb: 2 }}>
<Typography
component="label"
variant="body2"
sx={{
display: "block",
mb: 0.5,
fontWeight: 600,
color: "text.secondary",
}}
>
{label}
{required && (
<Box component="span" sx={{ color: "error.main", ml: 0.25 }}>
*
</Box>
)}
</Typography>
{children}
{error ? (
<Typography variant="caption" color="error">
{error}
</Typography>
) : hint ? (
<Typography variant="caption" color="text.secondary">
{hint}
</Typography>
) : null}
</Box>
);
}
/** Boolean switch with a label. */
export function SwitchField({
label,
checked,
onChange,
}: {
label: string;
checked: boolean;
onChange: (v: boolean) => void;
}) {
return (
<FormControlLabel
control={
<Switch
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
}
label={label}
/>
);
}

68
src/admin/ui/Modal.tsx Normal file
View File

@@ -0,0 +1,68 @@
import type { ReactNode } from "react";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import Typography from "@mui/material/Typography";
import MuiButton from "@mui/material/Button";
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
title: string;
subtitle?: string;
children: ReactNode;
loading?: boolean;
submitDisabled?: boolean;
submitText?: string;
cancelText?: string;
maxWidth?: "xs" | "sm" | "md" | "lg";
}
/** Form modal over MUI Dialog. Preserves the legacy FormModal prop shape. */
export default function Modal({
isOpen,
onClose,
onSubmit,
title,
subtitle,
children,
loading = false,
submitDisabled = false,
submitText = "Uložit",
cancelText = "Zrušit",
maxWidth = "sm",
}: ModalProps) {
return (
<Dialog
open={isOpen}
onClose={loading ? undefined : onClose}
fullWidth
maxWidth={maxWidth}
slotProps={{ paper: { sx: { borderRadius: 3 } } }}
>
<DialogTitle sx={{ pb: subtitle ? 0.5 : 1.5 }}>
{title}
{subtitle && (
<Typography variant="body2" color="text.secondary">
{subtitle}
</Typography>
)}
</DialogTitle>
<DialogContent>{children}</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<MuiButton onClick={onClose} color="inherit" disabled={loading}>
{cancelText}
</MuiButton>
<MuiButton
onClick={onSubmit}
variant="contained"
disabled={loading || submitDisabled}
>
{loading ? "Ukládám…" : submitText}
</MuiButton>
</DialogActions>
</Dialog>
);
}

View File

@@ -2,3 +2,8 @@ export { default as MuiProvider } from "./MuiProvider";
export { default as Button } from "./Button";
export { default as Card } from "./Card";
export { default as TextField } from "./TextField";
export { default as Modal } from "./Modal";
export { default as ConfirmDialog } from "./ConfirmDialog";
export { default as DataTable } from "./DataTable";
export type { DataColumn } from "./DataTable";
export { Field, SwitchField } from "./Field";