refactor(mui): remove dead legacy FormModal and ConfirmModal
Both components have zero importers after the phase-25 migrations (verified: no `./FormModal` or `./ConfirmModal` imports remain; build passes). Their MUI kit replacements are ui/Modal and ui/ConfirmDialog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,171 +0,0 @@
|
|||||||
import { useEffect, type ReactNode } from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import useReducedMotion from "../hooks/useReducedMotion";
|
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
|
|
||||||
interface ConfirmModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onConfirm: () => void;
|
|
||||||
title: string;
|
|
||||||
message: ReactNode;
|
|
||||||
confirmText?: string;
|
|
||||||
cancelText?: string;
|
|
||||||
type?: "danger" | "warning" | "default" | "info";
|
|
||||||
confirmVariant?: "danger" | "primary";
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfirmIcon({ type }: { type: string }) {
|
|
||||||
switch (type) {
|
|
||||||
case "danger":
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="15" y1="9" x2="9" y2="15" />
|
|
||||||
<line x1="9" y1="9" x2="15" y2="15" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case "info":
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="12" y1="16" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
case "warning":
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
||||||
<line x1="12" y1="9" x2="12" y2="13" />
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="12" y1="16" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ConfirmModal({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onConfirm,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
confirmText = "Potvrdit",
|
|
||||||
cancelText = "Zrušit",
|
|
||||||
type = "default",
|
|
||||||
confirmVariant,
|
|
||||||
loading,
|
|
||||||
}: ConfirmModalProps) {
|
|
||||||
const reducedMotion = useReducedMotion();
|
|
||||||
// Lock body scroll while open (ref-counted, nesting-safe) — matches FormModal.
|
|
||||||
useModalLock(isOpen);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape" && !loading) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [isOpen, loading, onClose]);
|
|
||||||
|
|
||||||
const duration = reducedMotion ? 0 : 0.22;
|
|
||||||
const ease = [0.16, 1, 0.3, 1] as const;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal-overlay"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="admin-modal-backdrop"
|
|
||||||
onClick={() => !loading && onClose()}
|
|
||||||
/>
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal admin-confirm-modal"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="confirm-modal-title"
|
|
||||||
initial={{ opacity: 0, scale: 0.96, y: 16 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.97, y: 8 }}
|
|
||||||
transition={{ duration, ease }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-body admin-confirm-content">
|
|
||||||
<div className={`admin-confirm-icon admin-confirm-icon-${type}`}>
|
|
||||||
<ConfirmIcon type={type} />
|
|
||||||
</div>
|
|
||||||
<h2 id="confirm-modal-title" className="admin-confirm-title">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<p className="admin-confirm-message">{message}</p>
|
|
||||||
</div>
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{cancelText}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onConfirm}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? "Zpracování..." : confirmText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import {
|
|
||||||
useEffect,
|
|
||||||
type ReactNode,
|
|
||||||
type FormEvent,
|
|
||||||
Children,
|
|
||||||
isValidElement,
|
|
||||||
cloneElement,
|
|
||||||
} from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import useModalLock from "../hooks/useModalLock";
|
|
||||||
import useReducedMotion from "../hooks/useReducedMotion";
|
|
||||||
|
|
||||||
export interface FormModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit?: () => void; // called when user clicks primary button (only if provided)
|
|
||||||
title: string;
|
|
||||||
/** Optional muted line shown under the title in the header. */
|
|
||||||
subtitle?: ReactNode;
|
|
||||||
children: ReactNode; // modal body (form fields)
|
|
||||||
submitLabel?: string; // primary button text
|
|
||||||
cancelLabel?: string; // cancel button text
|
|
||||||
loading?: boolean;
|
|
||||||
/** Disable the primary (submit) button — e.g. until the form is valid.
|
|
||||||
* Keeps the button visibly greyed out instead of clickable-but-inert. */
|
|
||||||
submitDisabled?: boolean;
|
|
||||||
hideFooter?: boolean; // for "view-only" modals
|
|
||||||
size?: "sm" | "md" | "lg"; // optional
|
|
||||||
closeOnBackdrop?: boolean; // default true
|
|
||||||
closeOnEsc?: boolean; // default true
|
|
||||||
/** Slot for footer-left actions (e.g. delete). Rendered to the LEFT of
|
|
||||||
* the standard cancel/submit group in the modal footer. */
|
|
||||||
footerLeft?: ReactNode;
|
|
||||||
/** Hide the close × in the header. Default false. */
|
|
||||||
hideCloseButton?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FormModal({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
children,
|
|
||||||
submitLabel = "Uložit",
|
|
||||||
cancelLabel = "Zrušit",
|
|
||||||
loading = false,
|
|
||||||
submitDisabled = false,
|
|
||||||
hideFooter = false,
|
|
||||||
size = "md",
|
|
||||||
closeOnBackdrop = true,
|
|
||||||
closeOnEsc = true,
|
|
||||||
footerLeft,
|
|
||||||
hideCloseButton = false,
|
|
||||||
}: FormModalProps) {
|
|
||||||
useModalLock(isOpen);
|
|
||||||
const reducedMotion = useReducedMotion();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen || !closeOnEsc) return;
|
|
||||||
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape" && !loading) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [isOpen, closeOnEsc, loading, onClose]);
|
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
|
||||||
if (closeOnBackdrop && !loading) onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrimaryClick = (e: FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (onSubmit) onSubmit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const titleId = "form-modal-title";
|
|
||||||
const modalClassName =
|
|
||||||
size === "lg" ? "admin-modal admin-modal-lg" : "admin-modal";
|
|
||||||
|
|
||||||
// Snappier ease curve for modal entry — matches the "industrial/refined"
|
|
||||||
// tone. Roughly equivalent to the cubic-bezier(0.16, 1, 0.3, 1) used by
|
|
||||||
// a lot of design systems for "expo out" feel.
|
|
||||||
const ease = [0.16, 1, 0.3, 1] as const;
|
|
||||||
const duration = reducedMotion ? 0 : 0.22;
|
|
||||||
// Stagger the body children slightly for a "drawing-in" reveal. We cap
|
|
||||||
// the cascade so long forms don't drag on forever.
|
|
||||||
const childrenArray = Children.toArray(children).filter(isValidElement);
|
|
||||||
const stagger = reducedMotion ? 0 : Math.min(childrenArray.length, 5) * 0.03;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isOpen && (
|
|
||||||
<motion.div
|
|
||||||
className="admin-modal-overlay"
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-backdrop" onClick={handleBackdropClick} />
|
|
||||||
<motion.div
|
|
||||||
className={modalClassName}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby={titleId}
|
|
||||||
initial={{ opacity: 0, scale: 0.96, y: 16 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.97, y: 8 }}
|
|
||||||
transition={{ duration, ease }}
|
|
||||||
>
|
|
||||||
<div className="admin-modal-header">
|
|
||||||
<div className="admin-modal-heading">
|
|
||||||
<h2 id={titleId} className="admin-modal-title">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
{subtitle && <p className="admin-modal-subtitle">{subtitle}</p>}
|
|
||||||
</div>
|
|
||||||
{!hideCloseButton && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="admin-modal-close"
|
|
||||||
onClick={() => !loading && onClose()}
|
|
||||||
aria-label="Zavřít"
|
|
||||||
title="Zavřít (Esc)"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18" />
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="admin-modal-body">
|
|
||||||
{reducedMotion || stagger === 0
|
|
||||||
? children
|
|
||||||
: Children.map(children, (child, i) => {
|
|
||||||
if (!isValidElement(child)) return child;
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 6 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{
|
|
||||||
duration: 0.2,
|
|
||||||
delay: 0.04 + i * 0.03,
|
|
||||||
ease: "easeOut",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{cloneElement(child)}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!hideFooter && (
|
|
||||||
<div className="admin-modal-footer">
|
|
||||||
<div className="admin-modal-footer-left">{footerLeft}</div>
|
|
||||||
<div className="admin-modal-footer-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => !loading && onClose()}
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{cancelLabel}
|
|
||||||
</button>
|
|
||||||
{onSubmit && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handlePrimaryClick}
|
|
||||||
className="admin-btn admin-btn-primary"
|
|
||||||
disabled={loading || submitDisabled}
|
|
||||||
>
|
|
||||||
{loading ? "Zpracování..." : submitLabel}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user