From 08c513ca26de056553995a6b673fc763bcca67fd Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 29 Sep 2026 12:43:52 +0200 Subject: [PATCH] feat(web): keep overlay content on screen while it animates closed Sheets and dialogs were closed by nulling the data they render, so they went blank, flipped titles or tabs, or dropped rows on the first frame of their exit. Add useClosingValue, which holds the last value until Base UI's onOpenChangeComplete(false), and move same-tick resets into onOpenChangeComplete. The edit-provider dialog and stylesheet color picker stay mounted so their exit transition runs. --- .../web/src/components/input/color-picker.tsx | 4 +- .../components/application-detail-sheet.tsx | 12 +- .../components/application-form-sheet.tsx | 19 +- .../components/detail/activity.tsx | 32 +++- .../components/import-applications-sheet.tsx | 12 +- .../features/documents/document-actions.tsx | 29 ++-- .../resume/editor/write/picture-settings.tsx | 13 +- .../src/features/resume/stylesheet/editor.tsx | 18 +- .../src/features/settings/account/data.tsx | 7 +- .../web/src/features/settings/ai/api-keys.tsx | 11 +- .../src/features/settings/ai/providers.tsx | 163 ++++++++++-------- apps/web/src/hooks/use-closing-value.test.ts | 31 ++++ apps/web/src/hooks/use-closing-value.ts | 18 ++ .../$resumeId/-components/share-sheet.tsx | 11 +- .../letter/-components/share-sheet.tsx | 11 +- 15 files changed, 264 insertions(+), 127 deletions(-) create mode 100644 apps/web/src/hooks/use-closing-value.test.ts create mode 100644 apps/web/src/hooks/use-closing-value.ts diff --git a/apps/web/src/components/input/color-picker.tsx b/apps/web/src/components/input/color-picker.tsx index 511ffd34e..c01da41d8 100644 --- a/apps/web/src/components/input/color-picker.tsx +++ b/apps/web/src/components/input/color-picker.tsx @@ -33,6 +33,7 @@ type ColorPickerProps = { onChange?: (value: string) => void; open?: boolean; onOpenChange?: React.ComponentProps["onOpenChange"]; + onOpenChangeComplete?: React.ComponentProps["onOpenChangeComplete"]; trigger?: React.ReactNode; children?: React.ReactNode; }; @@ -43,6 +44,7 @@ export function ColorPicker({ onChange, open, onOpenChange, + onOpenChangeComplete, trigger, children, }: ColorPickerProps) { @@ -60,7 +62,7 @@ export function ColorPicker({ } return ( - + {trigger ?? (
+ (() => (application ? toForm(application) : emptyForm())); @@ -127,7 +130,6 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) onSuccess: () => { invalidate(); toast.add({ type: "success", description: t`Application added to your pipeline.` }); - setForm(emptyForm()); onOpenChange(false); }, onError: () => toast.add({ type: "error", description: t`Couldn't add the application. Please try again.` }), @@ -195,7 +197,18 @@ export function ApplicationFormSheet({ open, onOpenChange, application }: Props) }; return ( - + { + onApplicationOpenChangeComplete(next); + // After adding, the fields clear once the sheet has closed; closing without saving keeps the draft. + if (!next && create.isSuccess) { + setForm(emptyForm()); + create.reset(); + } + }} + > {isEditing ? Edit application : Add application} diff --git a/apps/web/src/features/applications/components/detail/activity.tsx b/apps/web/src/features/applications/components/detail/activity.tsx index b04c9a556..e9f7d7e90 100644 --- a/apps/web/src/features/applications/components/detail/activity.tsx +++ b/apps/web/src/features/applications/components/detail/activity.tsx @@ -24,6 +24,7 @@ import { Icon } from "@reactive-resume/ui/components/icon"; import { Input } from "@reactive-resume/ui/components/input"; import { Textarea } from "@reactive-resume/ui/components/textarea"; import { toast } from "@reactive-resume/ui/components/toast"; +import { useClosingValue } from "@/hooks/use-closing-value"; import { useConfirm } from "@/hooks/use-confirm"; import { orpc } from "@/libs/orpc/client"; import { interviewKindOf } from "../../interviews"; @@ -53,6 +54,8 @@ export function Activity({ application, onOpenInterview }: ActivityProps) { const confirm = useConfirm(); const [note, setNote] = useState(""); const [editing, setEditing] = useState<{ entry: ApplicationTimelineEntry; date: string; text: string } | null>(null); + // Closing keeps the entry's title and fields on screen until the dialog has faded out. + const [shownEdit, onEditOpenChangeComplete] = useClosingValue(editing); const onError = () => toast.add({ type: "error", description: t`Couldn't update the timeline. Try again.` }); const addNote = useMutation({ @@ -194,22 +197,27 @@ export function Activity({ application, onOpenInterview }: ActivityProps) { })} - !open && setEditing(null)}> + !open && setEditing(null)} + onOpenChangeComplete={onEditOpenChangeComplete} + > - {editing?.entry.type === "note" ? Edit note : Edit date} + {shownEdit?.entry.type === "note" ? Edit note : Edit date} Change this timeline entry. - {editing && ( + {shownEdit && (
{ event.preventDefault(); + if (!editing) return; updateEntry.mutate({ id: application.id, entryId: editing.entry.id, @@ -218,19 +226,25 @@ export function Activity({ application, onOpenInterview }: ActivityProps) { }); }} > - {editing.entry.type === "note" && ( + {shownEdit.entry.type === "note" && (