From de53fde1f74ede8399f2426cc443c906a6c551ac Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Thu, 21 May 2026 12:41:14 -0700 Subject: [PATCH] Submit bar: animated stage-progress pills mirroring the header Bar's secondary line is now a horizontal cluster of the same six pills used in the page header, followed by the viewed stage label. Active pill expands and gains a slow halo (rail-pulse keyframe) when in the bar; same component runs without the pulse in the static header. Width transitions smoothly between ranks as the user scrolls, so the indicator visibly tracks progress through the form. StageProgress now takes an optional `pulse` prop and tightens its transition timing for nicer scroll-driven animation. --- components/EngagementForm.tsx | 47 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/components/EngagementForm.tsx b/components/EngagementForm.tsx index fe5acfd..364d027 100644 --- a/components/EngagementForm.tsx +++ b/components/EngagementForm.tsx @@ -204,11 +204,12 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) { return () => observer.disconnect(); }, [load.kind, config.sections]); - const viewedSectionLabel = useMemo(() => { - if (!viewedSectionId) return null; - const s = config.sections.find((x) => x.id === viewedSectionId); - return s?.label ?? null; - }, [viewedSectionId, config.sections]); + const viewedSection = useMemo( + () => (viewedSectionId ? config.sections.find((x) => x.id === viewedSectionId) ?? null : null), + [viewedSectionId, config.sections], + ); + const viewedSectionLabel = viewedSection?.label ?? null; + const viewedRank = viewedSection?.rank ?? null; // ── Auto-save draft on idle ──────────────────────────────────────────── // Debounce — save 1.5s after the user stops editing. Uses watch's @@ -411,6 +412,7 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) { draftSavedAt={draftSavedAt} orgName={load.data.orgName} viewedSectionLabel={viewedSectionLabel} + viewedRank={viewedRank} /> ); @@ -463,15 +465,22 @@ function resolveStageLabel( * Six small dots representing the six stages, with the current rank filled * and earlier ranks marked as visited. Quietly conveys progression without * pretending to be a "100% complete" progress bar. + * + * `pulse` adds a slow halo to the active pill — used in the floating + * submit bar (where the active rank moves as the user scrolls) to draw + * the eye to the changing indicator. Left off in the static header. */ -function StageProgress({ currentRank }: { currentRank: number }) { +function StageProgress({ currentRank, pulse = false }: { currentRank: number; pulse?: boolean }) { return (
{[0, 1, 2, 3, 4, 5].map((r) => ( ); - } else if (viewedSectionLabel) { + } else if (viewedSectionLabel && viewedRank != null) { secondary = ( -

- Viewing:{" "} - {viewedSectionLabel} - {isDirty && draftSavedAt && ( - · saved {formatRelative(draftSavedAt)} - )} -

+
+ +

+ {viewedSectionLabel} + {isDirty && draftSavedAt && ( + · saved {formatRelative(draftSavedAt)} + )} +

+
); } else if (isDirty && draftSavedAt) { secondary = (