refactor(ui): shared DraftSaveBar (Settings + Config save bars)
One Save/Discard bar with an inline (Settings card) and floating (Config page) variant, replacing the two near-identical hand-rolled bars + their state machine.
This commit is contained in:
parent
53c75f69e2
commit
f6b9ac2dd1
3 changed files with 109 additions and 66 deletions
75
frontend/src/components/ui/DraftSaveBar.tsx
Normal file
75
frontend/src/components/ui/DraftSaveBar.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Check, RotateCcw, Save } from "lucide-react";
|
||||
|
||||
// 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 wrap =
|
||||
variant === "floating"
|
||||
? "fixed bottom-4 left-1/2 -translate-x-1/2 z-40 glass rounded-2xl shadow-lg 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";
|
||||
return (
|
||||
<div className={wrap}>
|
||||
<span className="text-sm text-muted">
|
||||
{state === "saved" ? (
|
||||
<span className="text-emerald-500 inline-flex items-center gap-1.5">
|
||||
<Check className="w-4 h-4" />
|
||||
{labels.saved}
|
||||
</span>
|
||||
) : state === "error" ? (
|
||||
<span className="text-red-500">{labels.failed}</span>
|
||||
) : (
|
||||
labels.unsaved
|
||||
)}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onDiscard}
|
||||
disabled={saving || !dirty}
|
||||
className="glass-card glass-hover flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm disabled:opacity-40 transition"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
{labels.discard}
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={saving || !dirty}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm bg-accent text-accent-fg font-medium shadow-md shadow-accent/20 disabled:opacity-40 transition"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
{saving ? labels.saving : labels.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue