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:
57
src/admin/ui/ConfirmDialog.tsx
Normal file
57
src/admin/ui/ConfirmDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/admin/ui/DataTable.tsx
Normal file
82
src/admin/ui/DataTable.tsx
Normal 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
75
src/admin/ui/Field.tsx
Normal 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
68
src/admin/ui/Modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,8 @@ export { default as MuiProvider } from "./MuiProvider";
|
|||||||
export { default as Button } from "./Button";
|
export { default as Button } from "./Button";
|
||||||
export { default as Card } from "./Card";
|
export { default as Card } from "./Card";
|
||||||
export { default as TextField } from "./TextField";
|
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";
|
||||||
|
|||||||
Reference in New Issue
Block a user