feat(editor): View and Language menus — full desktop-menu parity
The menubar now matches the desktop app's native menu set: File, Edit, View, Language, Help. View collects what the user asked to reach from a menu: Compile (Ctrl+B), Run, Stop, Reset up top; the panel toggles — File Explorer, Output Console, Serial Monitor, Oscilloscope/Logic Analyzer — in the middle (store-backed ones render a live check, like a real desktop menu); and the canvas view actions (center, zoom) move here from Edit, which goes back to being undo/redo only, as menus have always worked. Language lists the nine locales with the current one checked, switching through the same switchLocale path the header globe uses — so language is reachable from the menubar regardless of the account state, on top of living in the signed-in account menu. Run/Stop/Compile/Reset and the console toggle register through the editorCommands seam from EditorToolbar (their handlers close over its state); the explorer toggle from EditorPage; serial and scope call their stores directly, same as undo/redo.
This commit is contained in:
parent
3b7abfc4f7
commit
d233b66cc9
|
|
@ -20,7 +20,11 @@
|
|||
*/
|
||||
import React, { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import { useOscilloscopeStore } from '../../store/useOscilloscopeStore';
|
||||
import { LOCALES, LOCALE_META, type Locale } from '../../i18n/config';
|
||||
import { getLocaleFromPath, switchLocale } from '../../i18n/path';
|
||||
import {
|
||||
hasEditorCommand,
|
||||
runEditorCommand,
|
||||
|
|
@ -48,12 +52,19 @@ const SITE = import.meta.env.VITE_PRO_BUILD ? '' : 'https://velxio.dev';
|
|||
|
||||
export const EditorMenuBar: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState<'file' | 'edit' | 'help' | null>(null);
|
||||
const [open, setOpen] = useState<'file' | 'edit' | 'view' | 'lang' | 'help' | null>(null);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Re-render when owners (un)register their commands.
|
||||
useSyncExternalStore(subscribeEditorCommands, getEditorCommandsVersion);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const currentLocale = getLocaleFromPath(location.pathname);
|
||||
const serialOpen = useSimulatorStore((s) => s.serialMonitorOpen);
|
||||
const toggleSerialMonitor = useSimulatorStore((s) => s.toggleSerialMonitor);
|
||||
const scopeOpen = useOscilloscopeStore((s) => s.open);
|
||||
const toggleOscilloscope = useOscilloscopeStore((s) => s.toggleOscilloscope);
|
||||
const undo = useSimulatorStore((s) => s.undo);
|
||||
const redo = useSimulatorStore((s) => s.redo);
|
||||
const history = useSimulatorStore((s) => s.history);
|
||||
|
|
@ -118,8 +129,19 @@ export const EditorMenuBar: React.FC = () => {
|
|||
{ kind: 'link', href: GITHUB_URL, label: t('editor.menu.github', 'GitHub Repository') },
|
||||
];
|
||||
|
||||
const editItems: Item[] = [
|
||||
{ kind: 'separator' }, // placeholder: undo/redo render specially above
|
||||
// Edit is undo/redo only (they render specially, with live history state);
|
||||
// everything view-shaped lives in the View menu, like the desktop app.
|
||||
const editItems: Item[] = [];
|
||||
|
||||
const viewItems: Item[] = [
|
||||
{ kind: 'command', id: 'sim.compile', label: t('editor.toolbar.compile', 'Compile'), shortcut: 'Ctrl+B' },
|
||||
{ kind: 'command', id: 'sim.run', label: t('editor.toolbar.run', 'Run') },
|
||||
{ kind: 'command', id: 'sim.stop', label: t('editor.toolbar.stop', 'Stop') },
|
||||
{ kind: 'command', id: 'sim.resetBoard', label: t('editor.toolbar.reset', 'Reset') },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', id: 'view.toggleExplorer', label: t('editor.menu.toggleExplorer', 'File Explorer') },
|
||||
{ kind: 'command', id: 'view.toggleConsole', label: t('editor.menu.toggleConsole', 'Output Console') },
|
||||
{ kind: 'separator' },
|
||||
{ kind: 'command', id: 'view.reset', label: t('editor.menu.centerView', 'Center canvas view') },
|
||||
{ kind: 'command', id: 'view.zoomIn', label: t('editor.canvas.zoomIn', 'Zoom in') },
|
||||
{ kind: 'command', id: 'view.zoomOut', label: t('editor.canvas.zoomOut', 'Zoom out') },
|
||||
|
|
@ -158,7 +180,7 @@ export const EditorMenuBar: React.FC = () => {
|
|||
</button>
|
||||
);
|
||||
|
||||
const menu = (which: 'file' | 'edit' | 'help', label: string, items: Item[]): React.ReactNode => (
|
||||
const menu = (which: 'file' | 'edit' | 'view' | 'lang' | 'help', label: string, items: Item[]): React.ReactNode => (
|
||||
<div className="emb-root" key={which}>
|
||||
<button
|
||||
className={`emb-trigger${open === which ? ' emb-trigger-open' : ''}`}
|
||||
|
|
@ -171,6 +193,59 @@ export const EditorMenuBar: React.FC = () => {
|
|||
</button>
|
||||
{open === which && (
|
||||
<div className="emb-menu" role="menu">
|
||||
{which === 'view' && (
|
||||
<>
|
||||
<button
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={serialOpen}
|
||||
className="emb-item"
|
||||
onClick={() => {
|
||||
setOpen(null);
|
||||
toggleSerialMonitor();
|
||||
}}
|
||||
>
|
||||
<span>{t('editor.canvas.toggleSerialMonitor', 'Serial Monitor')}</span>
|
||||
<span className="emb-shortcut">{serialOpen ? '✓' : ''}</span>
|
||||
</button>
|
||||
<button
|
||||
role="menuitemcheckbox"
|
||||
aria-checked={scopeOpen}
|
||||
className="emb-item"
|
||||
onClick={() => {
|
||||
setOpen(null);
|
||||
toggleOscilloscope();
|
||||
}}
|
||||
>
|
||||
<span>{t('editor.menu.toggleScope', 'Oscilloscope / Logic Analyzer')}</span>
|
||||
<span className="emb-shortcut">{scopeOpen ? '✓' : ''}</span>
|
||||
</button>
|
||||
<div className="emb-separator" />
|
||||
</>
|
||||
)}
|
||||
{which === 'lang' && (
|
||||
<>
|
||||
{LOCALES.map((loc) => (
|
||||
<button
|
||||
key={loc}
|
||||
role="menuitemradio"
|
||||
aria-checked={currentLocale === loc}
|
||||
className="emb-item"
|
||||
onClick={() => {
|
||||
setOpen(null);
|
||||
if (loc === currentLocale) return;
|
||||
navigate(
|
||||
switchLocale(location.pathname, loc as Locale) +
|
||||
location.search +
|
||||
location.hash,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span>{LOCALE_META[loc].nativeName}</span>
|
||||
<span className="emb-shortcut">{currentLocale === loc ? '✓' : ''}</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{which === 'edit' && (
|
||||
<>
|
||||
<button
|
||||
|
|
@ -217,6 +292,8 @@ export const EditorMenuBar: React.FC = () => {
|
|||
<div className="editor-menubar" ref={rootRef}>
|
||||
{menu('file', t('editor.menu.file', 'File'), fileItems)}
|
||||
{menu('edit', t('editor.menu.edit', 'Edit'), editItems)}
|
||||
{menu('view', t('editor.menu.view', 'View'), viewItems)}
|
||||
{menu('lang', t('editor.menu.language', 'Language'), [])}
|
||||
{menu('help', t('editor.menu.help', 'Help'), helpItems)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1395,6 +1395,11 @@ export const EditorToolbar = ({
|
|||
window.dispatchEvent(new CustomEvent('velxio-pro-replay-record-toggle', {
|
||||
detail: { projectId: currentProject?.id ?? null },
|
||||
})),
|
||||
compile: () => void handleCompile(),
|
||||
run: () => void handleRun(),
|
||||
stop: () => handleStop(),
|
||||
resetBoard: () => handleReset(),
|
||||
toggleConsole: () => setConsoleOpen((v) => !v),
|
||||
});
|
||||
const menuCommandsRef = useRef(makeMenuCommands());
|
||||
menuCommandsRef.current = makeMenuCommands();
|
||||
|
|
@ -1408,6 +1413,11 @@ export const EditorToolbar = ({
|
|||
registerEditorCommand('project.share', () => menuCommandsRef.current.share()),
|
||||
registerEditorCommand('project.githubSync', () => menuCommandsRef.current.githubSync()),
|
||||
registerEditorCommand('sim.record', () => menuCommandsRef.current.record()),
|
||||
registerEditorCommand('sim.compile', () => menuCommandsRef.current.compile()),
|
||||
registerEditorCommand('sim.run', () => menuCommandsRef.current.run()),
|
||||
registerEditorCommand('sim.stop', () => menuCommandsRef.current.stop()),
|
||||
registerEditorCommand('sim.resetBoard', () => menuCommandsRef.current.resetBoard()),
|
||||
registerEditorCommand('view.toggleConsole', () => menuCommandsRef.current.toggleConsole()),
|
||||
];
|
||||
return () => offs.forEach((off) => off());
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ export type EditorCommandId =
|
|||
| 'project.githubSync'
|
||||
| 'firmware.upload'
|
||||
| 'sim.record'
|
||||
| 'sim.compile'
|
||||
| 'sim.run'
|
||||
| 'sim.stop'
|
||||
| 'sim.resetBoard'
|
||||
| 'view.toggleExplorer'
|
||||
| 'view.toggleConsole'
|
||||
| 'view.reset'
|
||||
| 'view.zoomIn'
|
||||
| 'view.zoomOut';
|
||||
|
|
|
|||
|
|
@ -255,9 +255,13 @@ export const EditorPage: React.FC = () => {
|
|||
const offNew = registerEditorCommand('project.new', () => {
|
||||
void handleNewClick();
|
||||
});
|
||||
const offExplorer = registerEditorCommand('view.toggleExplorer', () =>
|
||||
setExplorerOpen((v) => !v),
|
||||
);
|
||||
return () => {
|
||||
offSave();
|
||||
offNew();
|
||||
offExplorer();
|
||||
};
|
||||
}, [handleSaveClick, handleNewClick]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue