From 990cced209e1509f5eba0470ec3d9a9e61e93594 Mon Sep 17 00:00:00 2001 From: David Montero Date: Tue, 18 Aug 2026 02:26:35 +0200 Subject: [PATCH] feat(editor): the Save button is the auto-save indicator; drop the header pill The header rendered a "Saved" / "Unsaved changes" text pill next to the menus whenever a project was loaded. It ate width in the one row that has to hold the whole editor toolbar, and it duplicated a signal the Save button in the workspace panel can carry by colour alone. The pill is gone. FileExplorer takes the AutoSaveState and colours its Save button: green saved, orange unsaved changes, pulsing while saving, red on failure; grey (unchanged) with no project loaded. The tooltip carries what the pill used to say plus the last-save time. Nine locales get the four new strings under editor.fileExplorer with English fallbacks. AppHeader no longer takes autoSave; EditorPage passes it to FileExplorer instead. No pro overlay code referenced the pill or the prop. --- .../src/components/editor/FileExplorer.css | 21 +++++++ .../src/components/editor/FileExplorer.tsx | 42 ++++++++++++- frontend/src/components/layout/AppHeader.tsx | 60 +------------------ frontend/src/i18n/locales/de/common2.json | 6 +- frontend/src/i18n/locales/en/common2.json | 6 +- frontend/src/i18n/locales/es/common2.json | 6 +- frontend/src/i18n/locales/fr/common2.json | 6 +- frontend/src/i18n/locales/it/common2.json | 6 +- frontend/src/i18n/locales/ja/common2.json | 6 +- frontend/src/i18n/locales/pt-br/common2.json | 6 +- frontend/src/i18n/locales/ru/common2.json | 6 +- frontend/src/i18n/locales/zh-cn/common2.json | 6 +- frontend/src/pages/EditorPage.tsx | 3 +- 13 files changed, 108 insertions(+), 72 deletions(-) diff --git a/frontend/src/components/editor/FileExplorer.css b/frontend/src/components/editor/FileExplorer.css index 659cf1f3..6b81ad73 100644 --- a/frontend/src/components/editor/FileExplorer.css +++ b/frontend/src/components/editor/FileExplorer.css @@ -67,6 +67,27 @@ color: var(--color-action-primary); } +/* The Save button doubles as the auto-save indicator once a project is + loaded (replaces the "Unsaved changes" text pill in the header). */ +.file-explorer-save-btn.is-saved { + color: #3fb950; +} +.file-explorer-save-btn.is-dirty { + color: #f0883e; +} +.file-explorer-save-btn.is-saving { + color: #3fb950; + animation: velxio-pulse 1s ease-in-out infinite; +} +.file-explorer-save-btn.is-error { + color: #f85149; +} +.file-explorer-save-btn.is-saved:hover, +.file-explorer-save-btn.is-dirty:hover, +.file-explorer-save-btn.is-error:hover { + filter: brightness(1.25); +} + .file-explorer-list { flex: 1; overflow-y: auto; diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx index 15c4d363..398b969e 100644 --- a/frontend/src/components/editor/FileExplorer.tsx +++ b/frontend/src/components/editor/FileExplorer.tsx @@ -1,6 +1,8 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import type { TFunction } from 'i18next'; import { useEditorStore, chipFileGroupId } from '../../store/useEditorStore'; +import type { AutoSaveState } from '../../hooks/useAutoSaveProject'; import type { WorkspaceFile } from '../../store/useEditorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { @@ -95,6 +97,26 @@ const IcoNewWorkspace = () => ( ); +/** Tooltip for the Save button: the plain action when no project is + * loaded, otherwise the auto-save status and the last save time. */ +function saveButtonTitle(t: TFunction, autoSave?: AutoSaveState): string { + const base = t('editor.fileExplorer.saveProject'); + if (!autoSave) return base; + const when = autoSave.lastSavedAt + ? ` — ${t('editor.fileExplorer.lastSaved', 'last saved')} ${new Date(autoSave.lastSavedAt).toLocaleTimeString()}` + : ''; + switch (autoSave.status) { + case 'dirty': + return `${base} — ${t('editor.fileExplorer.unsavedChanges')}${when}`; + case 'saving': + return `${base} — ${t('editor.fileExplorer.saving', 'saving…')}`; + case 'error': + return `${base} — ${t('editor.fileExplorer.saveFailed', 'save failed')}${autoSave.errorMessage ? `: ${autoSave.errorMessage}` : ''}`; + default: + return `${base} — ${t('editor.fileExplorer.saved', 'saved')}${when}`; + } +} + const IcoSave = () => ( void; onNewClick: () => void; + /** Auto-save state of the loaded project. When present, the Save button + * is the save indicator: green = saved, orange = unsaved changes, + * pulsing = saving, red = save failed. Replaces the text pill the header + * used to render ("Unsaved changes"), which ate toolbar width. */ + autoSave?: AutoSaveState; } -export const FileExplorer: React.FC = ({ onSaveClick, onNewClick }) => { +const SAVE_STATUS_CLASS: Record = { + idle: 'is-saved', + saved: 'is-saved', + dirty: 'is-dirty', + saving: 'is-saving', + error: 'is-error', +}; + +export const FileExplorer: React.FC = ({ onSaveClick, onNewClick, autoSave }) => { const { t } = useTranslation(); // Hidden
- - {meta.label} -
- ); -}; - -export const AppHeader: React.FC = ({ autoSave, editorMenu, editorToolbar }) => { +export const AppHeader: React.FC = ({ editorMenu, editorToolbar }) => { const location = useLocation(); const currentProject = useProjectStore((s) => s.currentProject); const [menuOpen, setMenuOpen] = useState(false); @@ -224,12 +177,7 @@ export const AppHeader: React.FC = ({ autoSave, editorMenu, edit {/* Editor toolbar strip — fills the middle the nav vacated. */} - {editorToolbar && ( - <> - {autoSave && currentProject && } -
{editorToolbar}
- - )} + {editorToolbar &&
{editorToolbar}
} {/* Right: language + share + auth + mobile hamburger. In the desktop-editor variant this block does not render at all: the @@ -242,10 +190,6 @@ export const AppHeader: React.FC = ({ autoSave, editorMenu, edit
- {/* Auto-save status — only when a project is loaded and the editor - page mounted the hook */} - {autoSave && currentProject && } - {/* Share button — visible when a project is loaded */} {currentProject && location.pathname === '/editor' && (