Editor has no 'last saved' indicator — Save button is enabled when paths exist but the user has no way to tell when the design was last persisted to the Gallery #215

Closed
opened 2026-10-04 00:03:29 +00:00 by Mark · 1 comment
Owner

Finding

src/components/editor/EditorHeader.tsx's Save button (line 220) is enabled when canSave is true (paths.some((p) => p.visible && p.nodes.length > 0)). Clicking it dispatches handleSave and toasts "Saved" or "Updated".

But the button always reads "Save" — there's no visual difference between "design has been edited since last save" and "design is clean." After a successful save, the user sees the toast (transient), and the Save button stays "Save" forever.

A user editing a design for 20 minutes:

  • Clicks Save at minute 5 (toast: "Saved")
  • Edits 10 more nodes
  • The Save button still says "Save" (not "Save *")
  • Clicks Save at minute 20
  • The toast: "Saved"
  • The button still says "Save"

There's no "this design is dirty" indicator.

grep -n 'isDirty\|lastSaved' src/components/editor/ — isDirty is the variable name in useEffect (per #161) but it's not exposed to the header.

Where

  • Save button: src/components/editor/EditorHeader.tsx:220-250
  • isDirty state: src/pages/Editor.tsx:684 (per #161)
  • No lastSaved timestamp: grep -n 'lastSaved\|savedAt' src/ returns 0
  • The save handler toasts but doesn't update any UI: src/pages/Editor.tsx:835 (addToast('success', ...))

Suggested fix shape

Two complementary changes:

  1. *"Save " indicator when the design is dirty:
<button onClick={onSave} disabled={!canSave}>
  <Save size={14} />
  {isDirty ? 'Save *' : 'Save'}
</button>

isDirty is already tracked (per #161); plumb it as a prop to EditorHeader.

  1. "Last saved 5 min ago" subtitle under the totals (the section that shows paths/stitches/stops):
{lastSaved && (
  <span className="text-[10px] text-[#6B6560]">
    Saved {formatDistanceToNow(lastSaved)} ago
  </span>
)}

lastSaved is set in the handleSave callback after a successful save. formatDistanceToNow is from date-fns (already a dependency per the rest of the app).

Both indicators reset on save and re-appear on edit. Combined, they tell the user "this design is dirty, you haven't saved in 5 minutes" without requiring the user to click Save and see the toast.

Severity

Low. The user can save and see the toast. The indicator is a polish improvement.

Discovered by

Gap analysis (2026-10-02) of editor save UX.

  • #161 (open) — Editor beforeunload. The isDirty flag is the same state.
  • #184 (open) — Editor back-to-workspace silent nav. Same unsaved-changes state.
## Finding `src/components/editor/EditorHeader.tsx`'s Save button (line 220) is enabled when `canSave` is true (`paths.some((p) => p.visible && p.nodes.length > 0)`). Clicking it dispatches `handleSave` and toasts "Saved" or "Updated". But the button always reads "Save" — there's no visual difference between "design has been edited since last save" and "design is clean." After a successful save, the user sees the toast (transient), and the Save button stays "Save" forever. A user editing a design for 20 minutes: - Clicks Save at minute 5 (toast: "Saved") - Edits 10 more nodes - The Save button still says "Save" (not "Save *") - Clicks Save at minute 20 - The toast: "Saved" - The button still says "Save" There's no "this design is dirty" indicator. `grep -n 'isDirty\|lastSaved' src/components/editor/` — `isDirty` is the variable name in `useEffect` (per #161) but it's not exposed to the header. ## Where - Save button: `src/components/editor/EditorHeader.tsx:220-250` - isDirty state: `src/pages/Editor.tsx:684` (per #161) - No lastSaved timestamp: `grep -n 'lastSaved\|savedAt' src/` returns 0 - The save handler toasts but doesn't update any UI: `src/pages/Editor.tsx:835` (`addToast('success', ...)`) ## Suggested fix shape Two complementary changes: 1. **"Save *" indicator** when the design is dirty: ```tsx <button onClick={onSave} disabled={!canSave}> <Save size={14} /> {isDirty ? 'Save *' : 'Save'} </button> ``` `isDirty` is already tracked (per #161); plumb it as a prop to `EditorHeader`. 2. **"Last saved 5 min ago" subtitle** under the totals (the section that shows paths/stitches/stops): ```tsx {lastSaved && ( <span className="text-[10px] text-[#6B6560]"> Saved {formatDistanceToNow(lastSaved)} ago </span> )} ``` `lastSaved` is set in the `handleSave` callback after a successful save. `formatDistanceToNow` is from `date-fns` (already a dependency per the rest of the app). Both indicators reset on save and re-appear on edit. Combined, they tell the user "this design is dirty, you haven't saved in 5 minutes" without requiring the user to click Save and see the toast. ## Severity Low. The user can save and see the toast. The indicator is a polish improvement. ## Discovered by Gap analysis (2026-10-02) of editor save UX. ## Related - #161 (open) — Editor beforeunload. The `isDirty` flag is the same state. - #184 (open) — Editor back-to-workspace silent nav. Same unsaved-changes state.
Author
Owner

Shipped in commit 7c36f20 on main.

Two complementary indicators:

  1. Save * indicator - the Save button now flips from Save to Save * when isDirty is true (Editor.tsx already tracked isDirty per #161; this just makes the header render it). Resets on save, re-appears on edit.

  2. Saved 5 min ago subtitle - appears under the path/stitch/stop totals once a successful save stamps lastSaved. Editor.tsx now tracks lastSaved (Date | null) and sets it from both handleSave and handleSaveAsNew (both write to the gallery, so both count). Uses date-fns formatDistanceToNow (already a dep, not previously imported in src/).

Both indicators stay hidden when totals.paths === 0 so the header does not claim a save against an empty design.

5 new tests cover the Save/Save * label, lastSaved rendering, lastSaved+paths, and the empty-design guard. 1056 vitest tests pass; tsc + eslint clean; vite build green.

Shipped in commit 7c36f20 on main. Two complementary indicators: 1. Save * indicator - the Save button now flips from Save to Save * when isDirty is true (Editor.tsx already tracked isDirty per #161; this just makes the header render it). Resets on save, re-appears on edit. 2. Saved 5 min ago subtitle - appears under the path/stitch/stop totals once a successful save stamps lastSaved. Editor.tsx now tracks lastSaved (Date | null) and sets it from both handleSave and handleSaveAsNew (both write to the gallery, so both count). Uses date-fns formatDistanceToNow (already a dep, not previously imported in src/). Both indicators stay hidden when totals.paths === 0 so the header does not claim a save against an empty design. 5 new tests cover the Save/Save * label, lastSaved rendering, lastSaved+paths, and the empty-design guard. 1056 vitest tests pass; tsc + eslint clean; vite build green.
Mark closed this issue 2026-10-04 04:08:35 +00:00
Sign in to join this conversation.
No description provided.