"use client"; import { useEffect } from "react"; import { Button } from "yt-dlp-transcript-common/components/ui/button"; import type { SaveResult } from "../actions"; // Save, and the thing this page never had: a signal that there is something to // save. Editing the tree used to change nothing visible, so navigating away // silently discarded the work — the classic "I clicked Move to top and it did // nothing" report. // // The Save button itself is ALWAYS rendered, never gated on dirtiness. That is // deliberate: three e2e specs click "Save policy" by name, and a button that // appears only under a condition is a button that can vanish under a race. The // dirty state is carried by the rail, the wording and the Discard button beside // it, which is where it belongs anyway. export function SaveBar({ dirty, saving, result, onSave, onDiscard, }: { dirty: boolean; saving: boolean; result: SaveResult | null; onSave: () => void; onDiscard: () => void; }) { // The browser-level guard. Only armed while there is something to lose. useEffect(() => { if (!dirty) return; const onBeforeUnload = (e: BeforeUnloadEvent) => e.preventDefault(); window.addEventListener("beforeunload", onBeforeUnload); return () => window.removeEventListener("beforeunload", onBeforeUnload); }, [dirty]); return (
{dirty && ( Unsaved changes )} {dirty && ( )} {/* The ONLY role="status" on a RUNNER page, twice over (one per lane). A spec asserts getByRole("status").first() reads exactly "Saved." on /operations/transcription, so any other live region there would break it. The boundary is RUNNER pages, and slice 3 is why that now has to be said: a sweep-fed operation's page carries settings forms with a role="status" of their own. Those pages have no policy tree and this bar never renders on them, so the two never meet — and the specs for those forms scope by form[data-settings-block] rather than by role. */} {result?.ok === true && ( Saved. )} {result?.ok === false && ( {result.error} )}
); }