fix(modals): commit FormModal footerLeft/hideCloseButton enhancements that committed code already depends on
PlanCellModal (and the modal system) reference FormModal's footerLeft slot and hideCloseButton, but those props lived only in an uncommitted working-tree change — so committed code did not typecheck (TS2322) and a build-from-committed (e.g. the v1.9.0 release) silently dropped the footer-left delete button in the plan entry/override edit modal. Commits the enhancements + their CSS so committed == working. NOTE: production v1.9.0 is affected (plan edit-modal delete button missing); needs a patch release. Flagged in the overnight audit report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
import { useEffect, type ReactNode, type FormEvent } from "react";
|
||||
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;
|
||||
@@ -15,6 +23,11 @@ export interface FormModalProps {
|
||||
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({
|
||||
@@ -30,8 +43,11 @@ export default function FormModal({
|
||||
size = "md",
|
||||
closeOnBackdrop = true,
|
||||
closeOnEsc = true,
|
||||
footerLeft,
|
||||
hideCloseButton = false,
|
||||
}: FormModalProps) {
|
||||
useModalLock(isOpen);
|
||||
const reducedMotion = useReducedMotion();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !closeOnEsc) return;
|
||||
@@ -59,6 +75,16 @@ export default function FormModal({
|
||||
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 && (
|
||||
@@ -67,7 +93,7 @@ export default function FormModal({
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
transition={{ duration: reducedMotion ? 0 : 0.18 }}
|
||||
>
|
||||
<div className="admin-modal-backdrop" onClick={handleBackdropClick} />
|
||||
<motion.div
|
||||
@@ -75,39 +101,86 @@ export default function FormModal({
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
initial={{ opacity: 0, scale: 0.96, y: 16 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
exit={{ opacity: 0, scale: 0.97, y: 8 }}
|
||||
transition={{ duration, ease }}
|
||||
>
|
||||
<div className="admin-modal-header">
|
||||
<h2 id={titleId} className="admin-modal-title">
|
||||
{title}
|
||||
</h2>
|
||||
{!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">{children}</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">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => !loading && onClose()}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
{onSubmit && (
|
||||
<div className="admin-modal-footer-left">{footerLeft}</div>
|
||||
<div className="admin-modal-footer-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrimaryClick}
|
||||
className="admin-btn admin-btn-primary"
|
||||
onClick={() => !loading && onClose()}
|
||||
className="admin-btn admin-btn-secondary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Zpracování..." : submitLabel}
|
||||
{cancelLabel}
|
||||
</button>
|
||||
)}
|
||||
{onSubmit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrimaryClick}
|
||||
className="admin-btn admin-btn-primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Zpracování..." : submitLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user