import { createPortal } from "react-dom"; import { Check, RotateCcw, Save } from "lucide-react"; import { Button } from "./Button"; import { SURFACE } from "./surfaces"; // The Save / Discard bar shown while an edited draft has unsaved changes (Settings prefs, // admin Configuration). Two visual variants share one state machine + markup: // - "inline": a bar pinned to the bottom of a card (Settings panel) // - "floating": a centered floating pill over the page (Configuration page) // Renders nothing while clean and idle; stays mounted through the fading saved/error message. export type SaveState = "idle" | "saving" | "saved" | "error"; export interface DraftSaveBarLabels { saved: string; failed: string; unsaved: string; discard: string; save: string; saving: string; } export function DraftSaveBar({ dirty, state, onSave, onDiscard, labels, variant = "inline", }: { dirty: boolean; state: SaveState; onSave: () => void; onDiscard: () => void; labels: DraftSaveBarLabels; variant?: "inline" | "floating"; }) { if (!dirty && state === "idle") return null; const saving = state === "saving"; const floating = variant === "floating"; const wrap = floating ? `fixed bottom-4 left-1/2 -translate-x-1/2 z-rail ${SURFACE.panel} flex items-center justify-between gap-4 px-4 py-3 w-[min(48rem,calc(100%-2rem))]` : "flex items-center justify-between gap-3 border-t border-border/60 px-4 py-3 bg-card/40"; // The floating variant is fixed to the viewport but renders from a page inside the page // scroller, so it goes to — otherwise the scroller's stacking context (its edge-fade // mask) scopes this z-rail and fades the bar's bottom edge. The inline variant is in normal flow // and stays put. const bar = (
{state === "saved" ? ( {labels.saved} ) : state === "error" ? ( {labels.failed} ) : ( labels.unsaved )}
); return floating ? createPortal(bar, document.body) : bar; }