diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx index 8fa14b2..fad57be 100644 --- a/frontend/src/components/ConfigPanel.tsx +++ b/frontend/src/components/ConfigPanel.tsx @@ -1,12 +1,13 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Check, RotateCcw, Save, Send } from "lucide-react"; +import { RotateCcw, Send } from "lucide-react"; import { api, type ConfigItem } from "../lib/api"; import { notify } from "../lib/notifications"; import Tooltip from "./Tooltip"; import Tabs, { usePersistedTab } from "./Tabs"; import { Switch } from "./ui/form"; +import { DraftSaveBar } from "./ui/DraftSaveBar"; // Admin Configuration page: edit DB-overridable operational settings (registry-driven from the // backend — adding a key there makes it appear here automatically). Edits are drafted and @@ -151,40 +152,21 @@ export default function ConfigPanel() { )} - {(dirty || saveState !== "idle") && ( -
- - {saveState === "saved" ? ( - - - {t("config.saved")} - - ) : saveState === "error" ? ( - {t("config.saveFailed")} - ) : ( - t("config.unsaved") - )} - -
- - -
-
- )} + ); } diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index 8227a37..4d13622 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; -import { Bell, Check, Monitor, RotateCcw, Save, Trash2, User } from "lucide-react"; +import { Bell, Monitor, Trash2, User } from "lucide-react"; import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme"; import { api, type Me } from "../lib/api"; import { LS, usePersistedState } from "../lib/storage"; @@ -9,6 +9,7 @@ import Avatar from "./Avatar"; import { notify, type NotifSettings } from "../lib/notifications"; import Tooltip from "./Tooltip"; import { Section, SettingRow, Switch } from "./ui/form"; +import { DraftSaveBar } from "./ui/DraftSaveBar"; import { useConfirm } from "./ConfirmProvider"; // The Settings page edits server-persisted preferences as a draft: changes apply locally @@ -104,37 +105,22 @@ export default function SettingsPanel({ function PrefsSaveBar({ prefs }: { prefs: PrefsController }) { const { t } = useTranslation(); - // Stay mounted while a transient "saved"/"error" message is fading, even after dirty clears. - if (!prefs.dirty && prefs.saveState === "idle") return null; - const saving = prefs.saveState === "saving"; return ( -
- - {prefs.saveState === "saved" - ? {t("settings.save.saved")} - : prefs.saveState === "error" - ? {t("settings.save.failed")} - : t("settings.save.unsaved")} - -
- - -
-
+ ); } diff --git a/frontend/src/components/ui/DraftSaveBar.tsx b/frontend/src/components/ui/DraftSaveBar.tsx new file mode 100644 index 0000000..5e629af --- /dev/null +++ b/frontend/src/components/ui/DraftSaveBar.tsx @@ -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 ( +
+ + {state === "saved" ? ( + + + {labels.saved} + + ) : state === "error" ? ( + {labels.failed} + ) : ( + labels.unsaved + )} + +
+ + +
+
+ ); +}