import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { Trans, useLingui } from '@lingui/react/macro'; import { AnimatePresence, motion } from 'framer-motion'; import { AlertTriangleIcon } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; export type FormStickySaveBarProps = { isDirty: boolean; isSubmitting: boolean; onReset: () => void; }; /** * A single `position: sticky` bar rendered at the bottom of the form. * * - When the form's end is on screen it settles into place as a plain footer (just the * Reset / Save buttons). * - When the form's end is scrolled off, it sticks to the bottom of the viewport and * shows the "unsaved changes" pill chrome. * * Because it's the same element in the form's flow, it auto-aligns to the form and the * float <-> dock hand-off is a native, scroll-linked transition (no measurement, no * shared-layout morph). A 1px sentinel below it detects the stuck state so we can toggle * the pill chrome. */ export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset }: FormStickySaveBarProps) => { const { t } = useLingui(); const sentinelRef = useRef(null); const [isStuck, setIsStuck] = useState(false); useEffect(() => { const sentinel = sentinelRef.current; if (!sentinel) { return; } // The sentinel sits at the bar's resting position (the end of the form). While the // bar is stuck to the bottom of the viewport the sentinel is scrolled past (out of // view); once you reach the form's end it comes into view and the bar settles. const observer = new IntersectionObserver( ([entry]) => { setIsStuck(!entry.isIntersecting); }, { root: null, rootMargin: '0px 0px -24px 0px', threshold: 0, }, ); observer.observe(sentinel); return () => { observer.disconnect(); }; }, []); // Show the floating pill chrome only when there are unsaved changes AND the form's // end is off screen. const isFloating = isDirty && isStuck; return ( <>
{isFloating && ( You have unsaved changes )}
{isDirty && ( )}
{/* Sentinel: detects when the sticky bar is floating (stuck) vs settled (docked). */}
); };