diff --git a/apps/web/src/components/DiagramEditorPane.test.mjs b/apps/web/src/components/DiagramEditorPane.test.mjs index fb838590..32e28c2b 100644 --- a/apps/web/src/components/DiagramEditorPane.test.mjs +++ b/apps/web/src/components/DiagramEditorPane.test.mjs @@ -59,6 +59,11 @@ describe("diagram editor canvas surface", () => { expect(source).toContain("!readOnly && saveFailed"); expect(source).toContain('t("diagram.retrySave")'); expect(source).not.toContain(''); + expect(source).toContain('t("editor.saveState.unsaved")'); + expect(source).toContain('"bg-slate-100 text-slate-500"'); + expect(source).toContain("{...statusSettleMotion}"); + expect(source).toContain('saveStatus === "unsaved" ? ('); + expect(source).not.toContain('t("diagram.saved")'); }); test("uses a clean grid-free canvas for both diagram types", () => { diff --git a/apps/web/src/components/DiagramEditorPane.tsx b/apps/web/src/components/DiagramEditorPane.tsx index 0be4b7c6..ab46b598 100644 --- a/apps/web/src/components/DiagramEditorPane.tsx +++ b/apps/web/src/components/DiagramEditorPane.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Export, Graph, History, Keyboard, Selection, type Edge, type Node } from "@antv/x6"; +import * as m from "motion/react-m"; import { Activity, AppWindow, @@ -105,6 +106,7 @@ import { compactArchitectureNodeSize, compactFlowchartNodeSize, compactMindMapNo import { resolveDiagramPalette, type DiagramAppearance } from "@/lib/diagram-theme"; import { isLocalMemoId } from "@/lib/local-mirror"; import { isBrowserOffline } from "@/lib/network-status"; +import { statusSettleMotion } from "@/lib/motion"; import type { EdgeEverRepository } from "@/lib/repository"; import { cn, formatDateTime } from "@/lib/utils"; @@ -1938,6 +1940,19 @@ export const DiagramEditorPane = ({ const currentMarkdown = historyOpen ? serializeDiagramDocument(graphRef.current ? graphToDocument(graphRef.current, document.kind, themeRef.current) : document) : memo.contentMarkdown; + const saveStatus = saveError ? "error" : saving ? "saving" : dirty ? "unsaved" : "saved"; + const saveLabel = saveStatus === "error" + ? t("editor.saveState.error") + : saveStatus === "saving" + ? t("editor.saveState.saving") + : saveStatus === "unsaved" + ? t("editor.saveState.unsaved") + : t("editor.saveState.saved"); + const saveStatusClassName = saveStatus === "error" + ? "bg-rose-50 text-rose-700" + : saveStatus === "saved" + ? "bg-slate-100 text-slate-500" + : "bg-emerald-50 text-emerald-700"; return ( @@ -2001,19 +2016,41 @@ export const DiagramEditorPane = ({
- - {saveError ?? (dirty || saving ? t("diagram.saving") : t("diagram.saved"))} - - + {saveLabel} + + - {saveError ? : dirty || saving ? : } - {saveError ?? (dirty || saving ? t("diagram.saving") : t("diagram.saved"))} - + saveStatusClassName, + )} + role="status" + aria-live="polite" + aria-label={saveError ? `${saveLabel}. ${saveError}` : undefined} + {...statusSettleMotion} + > + {saveStatus === "error" ? ( +