diff --git a/src/admin/ui/ConfirmDialog.tsx b/src/admin/ui/ConfirmDialog.tsx
new file mode 100644
index 0000000..bd0f935
--- /dev/null
+++ b/src/admin/ui/ConfirmDialog.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/admin/ui/DataTable.tsx b/src/admin/ui/DataTable.tsx
new file mode 100644
index 0000000..b4126c9
--- /dev/null
+++ b/src/admin/ui/DataTable.tsx
@@ -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 {
+ 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 {
+ columns: DataColumn[];
+ 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({
+ columns,
+ rows,
+ rowKey,
+ rowInactive,
+ empty,
+}: DataTableProps) {
+ if (rows.length === 0 && empty) return <>{empty}>;
+ return (
+
+
+
+
+ {columns.map((c) => (
+
+ {c.header}
+
+ ))}
+
+
+
+ {rows.map((row) => (
+
+ {columns.map((c) => (
+
+ {c.render(row)}
+
+ ))}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/admin/ui/Field.tsx b/src/admin/ui/Field.tsx
new file mode 100644
index 0000000..e2d29d7
--- /dev/null
+++ b/src/admin/ui/Field.tsx
@@ -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 (
+
+
+ {label}
+ {required && (
+
+ *
+
+ )}
+
+ {children}
+ {error ? (
+
+ {error}
+
+ ) : hint ? (
+
+ {hint}
+
+ ) : null}
+
+ );
+}
+
+/** Boolean switch with a label. */
+export function SwitchField({
+ label,
+ checked,
+ onChange,
+}: {
+ label: string;
+ checked: boolean;
+ onChange: (v: boolean) => void;
+}) {
+ return (
+ onChange(e.target.checked)}
+ />
+ }
+ label={label}
+ />
+ );
+}
diff --git a/src/admin/ui/Modal.tsx b/src/admin/ui/Modal.tsx
new file mode 100644
index 0000000..d720a0b
--- /dev/null
+++ b/src/admin/ui/Modal.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/admin/ui/index.ts b/src/admin/ui/index.ts
index d5002f3..d904161 100644
--- a/src/admin/ui/index.ts
+++ b/src/admin/ui/index.ts
@@ -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";