From 86e4cbf3456cf30d889d2b3f194d3db8257d5795 Mon Sep 17 00:00:00 2001 From: BOHA Date: Sat, 6 Jun 2026 01:56:18 +0200 Subject: [PATCH] fix(modals): commit FormModal footerLeft/hideCloseButton enhancements that committed code already depends on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/admin/components.css | 75 ++++++++++++++++- src/admin/components/ConfirmModal.tsx | 13 ++- src/admin/components/FormModal.tsx | 111 +++++++++++++++++++++----- 3 files changed, 175 insertions(+), 24 deletions(-) diff --git a/src/admin/components.css b/src/admin/components.css index 5430138..bd9fa6d 100644 --- a/src/admin/components.css +++ b/src/admin/components.css @@ -221,6 +221,8 @@ position: absolute; inset: 0; background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(2px); + -webkit-backdrop-filter: blur(2px); touch-action: none; } @@ -247,6 +249,10 @@ padding: 18px; border-bottom: 1px solid var(--border-color); flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } .admin-modal-title { @@ -255,6 +261,38 @@ color: var(--text-primary); } +.admin-modal-close { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + background: transparent; + border: 1px solid transparent; + color: var(--text-muted); + border-radius: var(--border-radius-sm); + cursor: pointer; + transition: + background var(--motion-fast) ease, + color var(--motion-fast) ease, + border-color var(--motion-fast) ease, + transform var(--motion-fast) ease; +} + +.admin-modal-close:hover:not(:disabled) { + background: var(--bg-tertiary); + color: var(--text-primary); + border-color: var(--border-color); + transform: rotate(90deg); +} + +.admin-modal-close:disabled { + opacity: 0.5; + cursor: not-allowed; +} + .admin-modal-body { padding: 18px; overflow-y: auto; @@ -269,10 +307,45 @@ border-top: 1px solid var(--border-color); display: flex; gap: 0.75rem; - justify-content: flex-end; + justify-content: space-between; + align-items: center; flex-shrink: 0; } +.admin-modal-footer-left, +.admin-modal-footer-right { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.admin-modal-footer-right { + margin-left: auto; +} + +@media (max-width: 480px) { + .admin-modal-footer { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + .admin-modal-footer-left, + .admin-modal-footer-right { + width: 100%; + } + .admin-modal-footer-left .admin-btn, + .admin-modal-footer-right .admin-btn { + flex: 1; + } + .admin-modal-footer-left { + order: 2; + } + .admin-modal-footer-right { + order: 1; + margin-left: 0; + } +} + @media (max-width: 768px) { .admin-modal-overlay { padding: 0; diff --git a/src/admin/components/ConfirmModal.tsx b/src/admin/components/ConfirmModal.tsx index f2c27cd..fb4f692 100644 --- a/src/admin/components/ConfirmModal.tsx +++ b/src/admin/components/ConfirmModal.tsx @@ -1,5 +1,6 @@ import { useEffect, type ReactNode } from "react"; import { motion, AnimatePresence } from "framer-motion"; +import useReducedMotion from "../hooks/useReducedMotion"; interface ConfirmModalProps { isOpen: boolean; @@ -91,6 +92,7 @@ export default function ConfirmModal({ confirmVariant, loading, }: ConfirmModalProps) { + const reducedMotion = useReducedMotion(); useEffect(() => { if (!isOpen) return; @@ -104,6 +106,9 @@ export default function ConfirmModal({ 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 ( {isOpen && ( @@ -112,7 +117,7 @@ export default function ConfirmModal({ initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - transition={{ duration: 0.2 }} + transition={{ duration: reducedMotion ? 0 : 0.18 }} >
diff --git a/src/admin/components/FormModal.tsx b/src/admin/components/FormModal.tsx index 7b9fe0d..8431c25 100644 --- a/src/admin/components/FormModal.tsx +++ b/src/admin/components/FormModal.tsx @@ -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 ( {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 }} >

{title}

+ {!hideCloseButton && ( + + )}
-
{children}
+
+ {reducedMotion || stagger === 0 + ? children + : Children.map(children, (child, i) => { + if (!isValidElement(child)) return child; + return ( + + {cloneElement(child)} + + ); + })} +
{!hideFooter && (
- - {onSubmit && ( +
{footerLeft}
+
- )} + {onSubmit && ( + + )} +
)}