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.
This commit is contained in:
parent
a0aeb101cd
commit
990cced209
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
/** 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 = () => (
|
||||
<svg
|
||||
width="22"
|
||||
|
|
@ -346,9 +368,22 @@ interface ContextMenu {
|
|||
interface FileExplorerProps {
|
||||
onSaveClick: () => 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<FileExplorerProps> = ({ onSaveClick, onNewClick }) => {
|
||||
const SAVE_STATUS_CLASS: Record<AutoSaveState['status'], string> = {
|
||||
idle: 'is-saved',
|
||||
saved: 'is-saved',
|
||||
dirty: 'is-dirty',
|
||||
saving: 'is-saving',
|
||||
error: 'is-error',
|
||||
};
|
||||
|
||||
export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewClick, autoSave }) => {
|
||||
const { t } = useTranslation();
|
||||
// Hidden <input type="file"> we trigger via ref when the user clicks
|
||||
// the Open project button. Accepts both .vlx (Velxio native) and .zip
|
||||
|
|
@ -763,8 +798,9 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({ onSaveClick, onNewCl
|
|||
style={{ display: 'none' }}
|
||||
/>
|
||||
<button
|
||||
className="file-explorer-save-btn"
|
||||
title={t('editor.fileExplorer.saveProject')}
|
||||
className={`file-explorer-save-btn${autoSave ? ` ${SAVE_STATUS_CLASS[autoSave.status]}` : ''}`}
|
||||
title={saveButtonTitle(t, autoSave)}
|
||||
aria-label={saveButtonTitle(t, autoSave)}
|
||||
onClick={onSaveClick}
|
||||
>
|
||||
<IcoSave />
|
||||
|
|
|
|||
|
|
@ -7,15 +7,12 @@ import { LanguageSwitcher } from './LanguageSwitcher';
|
|||
import { useLocalizedHref, useCurrentLocale } from '../../i18n/useLocalizedNavigate';
|
||||
import { blogUrlFor } from '../../i18n/path';
|
||||
import { trackVisitGitHub, trackVisitDiscord } from '../../utils/analytics';
|
||||
import type { AutoSaveState } from '../../hooks/useAutoSaveProject';
|
||||
import './LanguageSwitcher.css';
|
||||
|
||||
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
|
||||
const DISCORD_URL = 'https://discord.gg/3mARjJrh4E';
|
||||
|
||||
interface AppHeaderProps {
|
||||
/** Optional auto-save state — when set, renders a save status indicator. */
|
||||
autoSave?: AutoSaveState;
|
||||
/** Editor variant: a File/Edit menu bar rendered next to the logo. When
|
||||
* set, the marketing nav links (Home / Docs / Pricing / …) are hidden —
|
||||
* inside the editor they are noise that costs exactly the width the
|
||||
|
|
@ -29,51 +26,7 @@ interface AppHeaderProps {
|
|||
editorToolbar?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SAVE_STATUS_COPY: Record<AutoSaveState['status'], { label: string; color: string }> = {
|
||||
idle: { label: 'Saved', color: '#7d8590' },
|
||||
dirty: { label: 'Unsaved changes', color: '#f0883e' },
|
||||
saving: { label: 'Saving…', color: '#3fb950' },
|
||||
saved: { label: 'Saved', color: '#3fb950' },
|
||||
error: { label: 'Save failed', color: '#f85149' },
|
||||
};
|
||||
|
||||
const AutoSaveIndicator: React.FC<{ state: AutoSaveState }> = ({ state }) => {
|
||||
const meta = SAVE_STATUS_COPY[state.status];
|
||||
const tip =
|
||||
state.status === 'error' && state.errorMessage
|
||||
? `Auto-save failed: ${state.errorMessage}`
|
||||
: state.lastSavedAt
|
||||
? `Last saved ${new Date(state.lastSavedAt).toLocaleTimeString()}`
|
||||
: 'Auto-save ready';
|
||||
return (
|
||||
<div
|
||||
title={tip}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '4px 10px',
|
||||
fontSize: 12,
|
||||
color: meta.color,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: '50%',
|
||||
background: meta.color,
|
||||
opacity: state.status === 'saving' ? 0.7 : 1,
|
||||
animation: state.status === 'saving' ? 'velxio-pulse 1s ease-in-out infinite' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span>{meta.label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppHeader: React.FC<AppHeaderProps> = ({ autoSave, editorMenu, editorToolbar }) => {
|
||||
export const AppHeader: React.FC<AppHeaderProps> = ({ 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<AppHeaderProps> = ({ autoSave, editorMenu, edit
|
|||
</div>
|
||||
|
||||
{/* Editor toolbar strip — fills the middle the nav vacated. */}
|
||||
{editorToolbar && (
|
||||
<>
|
||||
{autoSave && currentProject && <AutoSaveIndicator state={autoSave} />}
|
||||
<div className="header-editor-toolbar">{editorToolbar}</div>
|
||||
</>
|
||||
)}
|
||||
{editorToolbar && <div className="header-editor-toolbar">{editorToolbar}</div>}
|
||||
|
||||
{/* 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<AppHeaderProps> = ({ autoSave, editorMenu, edit
|
|||
<div className="header-right">
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Auto-save status — only when a project is loaded and the editor
|
||||
page mounted the hook */}
|
||||
{autoSave && currentProject && <AutoSaveIndicator state={autoSave} />}
|
||||
|
||||
{/* Share button — visible when a project is loaded */}
|
||||
{currentProject && location.pathname === '/editor' && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Umbenennen",
|
||||
"delete": "Löschen"
|
||||
}
|
||||
},
|
||||
"lastSaved": "zuletzt gespeichert",
|
||||
"saving": "wird gespeichert…",
|
||||
"saved": "gespeichert",
|
||||
"saveFailed": "Speichern fehlgeschlagen"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Ungespeicherte Änderungen",
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Rename",
|
||||
"delete": "Delete"
|
||||
}
|
||||
},
|
||||
"lastSaved": "last saved",
|
||||
"saving": "saving…",
|
||||
"saved": "saved",
|
||||
"saveFailed": "save failed"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Unsaved changes",
|
||||
|
|
|
|||
|
|
@ -74,7 +74,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Renombrar",
|
||||
"delete": "Eliminar"
|
||||
}
|
||||
},
|
||||
"lastSaved": "guardado por última vez",
|
||||
"saving": "guardando…",
|
||||
"saved": "guardado",
|
||||
"saveFailed": "error al guardar"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Cambios sin guardar",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Renommer",
|
||||
"delete": "Supprimer"
|
||||
}
|
||||
},
|
||||
"lastSaved": "dernier enregistrement",
|
||||
"saving": "enregistrement…",
|
||||
"saved": "enregistré",
|
||||
"saveFailed": "échec de l'enregistrement"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Modifications non enregistrées",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Rinomina",
|
||||
"delete": "Elimina"
|
||||
}
|
||||
},
|
||||
"lastSaved": "ultimo salvataggio",
|
||||
"saving": "salvataggio…",
|
||||
"saved": "salvato",
|
||||
"saveFailed": "salvataggio non riuscito"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Modifiche non salvate",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "名前変更",
|
||||
"delete": "削除"
|
||||
}
|
||||
},
|
||||
"lastSaved": "最終保存",
|
||||
"saving": "保存中…",
|
||||
"saved": "保存済み",
|
||||
"saveFailed": "保存に失敗しました"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "未保存の変更",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Renomear",
|
||||
"delete": "Excluir"
|
||||
}
|
||||
},
|
||||
"lastSaved": "salvo pela última vez",
|
||||
"saving": "salvando…",
|
||||
"saved": "salvo",
|
||||
"saveFailed": "falha ao salvar"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Alterações não salvas",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить"
|
||||
}
|
||||
},
|
||||
"lastSaved": "последнее сохранение",
|
||||
"saving": "сохранение…",
|
||||
"saved": "сохранено",
|
||||
"saveFailed": "ошибка сохранения"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "Несохранённые изменения",
|
||||
|
|
|
|||
|
|
@ -72,7 +72,11 @@
|
|||
"contextMenu": {
|
||||
"rename": "重命名",
|
||||
"delete": "删除"
|
||||
}
|
||||
},
|
||||
"lastSaved": "上次保存",
|
||||
"saving": "保存中…",
|
||||
"saved": "已保存",
|
||||
"saveFailed": "保存失败"
|
||||
},
|
||||
"fileTabs": {
|
||||
"unsavedChanges": "未保存的更改",
|
||||
|
|
|
|||
|
|
@ -544,7 +544,6 @@ export const EditorPage: React.FC = () => {
|
|||
return (
|
||||
<div className="app">
|
||||
<AppHeader
|
||||
autoSave={autoSave}
|
||||
editorMenu={!isMobile ? <EditorMenuBar /> : undefined}
|
||||
editorToolbar={unifiedToolbar}
|
||||
/>
|
||||
|
|
@ -628,7 +627,7 @@ export const EditorPage: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minHeight: 0, display: 'flex', overflow: 'hidden' }}>
|
||||
<FileExplorer onSaveClick={handleSaveClick} onNewClick={handleNewClick} />
|
||||
<FileExplorer onSaveClick={handleSaveClick} onNewClick={handleNewClick} autoSave={autoSave} />
|
||||
</div>
|
||||
{accountBlock && (
|
||||
<div className="explorer-account-footer">{accountBlock}</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue