feat(ui): confirm ending a show and surface help

End-session buttons, Escape, and window close share one confirm. Help is a question-mark icon on the control panel and after the version. About opens directly. Scene editor follows the UI language.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-10-02 08:10:54 +08:00
parent d228b9aab7
commit 060bbd471a
12 changed files with 245 additions and 176 deletions
+15 -66
View File
@@ -130,7 +130,6 @@ export function EditorApp() {
const [projectMenuOpen, setProjectMenuOpen] = useState(false);
const [settingsMenuOpen, setSettingsMenuOpen] = useState(false);
const [settingsLangSubOpen, setSettingsLangSubOpen] = useState(false);
const [aboutMenuOpen, setAboutMenuOpen] = useState(false);
const [appAboutOpen, setAppAboutOpen] = useState(false);
const [renameOpen, setRenameOpen] = useState(false);
const [exportModalOpen, setExportModalOpen] = useState(false);
@@ -279,11 +278,9 @@ export function EditorApp() {
const fileMenuBtnRef = useRef<HTMLButtonElement | null>(null);
const projectMenuBtnRef = useRef<HTMLButtonElement | null>(null);
const settingsMenuBtnRef = useRef<HTMLButtonElement | null>(null);
const aboutMenuBtnRef = useRef<HTMLButtonElement | null>(null);
const [fileMenuPos, setFileMenuPos] = useState<{ left: number; top: number } | null>(null);
const [projectMenuPos, setProjectMenuPos] = useState<{ left: number; top: number } | null>(null);
const [settingsMenuPos, setSettingsMenuPos] = useState<{ left: number; top: number } | null>(null);
const [aboutMenuPos, setAboutMenuPos] = useState<{ left: number; top: number } | null>(null);
const [selectedGraphNodeId, setSelectedGraphNodeId] = useState<GraphNodeId | null>(null);
const [sceneListDrop, setSceneListDrop] = useState<{
targetId: SceneId;
@@ -489,26 +486,6 @@ export function EditorApp() {
if (!settingsMenuOpen) setSettingsLangSubOpen(false);
}, [settingsMenuOpen]);
useEffect(() => {
if (!aboutMenuOpen) return;
const r = aboutMenuBtnRef.current?.getBoundingClientRect() ?? null;
queueMicrotask(() => {
if (r) {
setAboutMenuPos({ left: r.left, top: r.bottom + 10 });
} else {
setAboutMenuPos(null);
}
});
const onDown = (e: MouseEvent) => {
const t = e.target as HTMLElement | null;
if (!t) return;
if (t.closest('[data-aboutmenu-root="1"]')) return;
setAboutMenuOpen(false);
};
window.addEventListener('mousedown', onDown);
return () => window.removeEventListener('mousedown', onDown);
}, [aboutMenuOpen]);
useEffect(() => {
if (!runMenuOpen) return;
const r = runSplitRef.current?.getBoundingClientRect() ?? null;
@@ -818,7 +795,6 @@ export function EditorApp() {
onClick={() => {
setFileMenuOpen(false);
setProjectMenuOpen(false);
setAboutMenuOpen(false);
setSettingsMenuOpen((v) => {
const next = !v;
if (next) setSettingsLangSubOpen(false);
@@ -829,16 +805,14 @@ export function EditorApp() {
{t('top.settings')}
</button>
<button
ref={aboutMenuBtnRef}
type="button"
data-aboutmenu-root="1"
className={styles.fileMenuTrigger}
onClick={() => {
setFileMenuOpen(false);
setProjectMenuOpen(false);
setSettingsMenuOpen(false);
setSettingsLangSubOpen(false);
setAboutMenuOpen((v) => !v);
setAppAboutOpen(true);
}}
>
{t('top.aboutApp')}
@@ -854,7 +828,6 @@ export function EditorApp() {
if (!licenseActive) return;
setFileMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setProjectMenuOpen((v) => !v);
}}
>
@@ -872,7 +845,6 @@ export function EditorApp() {
if (!licenseActive) return;
setProjectMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setFileMenuOpen((v) => !v);
}}
>
@@ -890,7 +862,6 @@ export function EditorApp() {
if (!licenseActive) return;
setProjectMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setFileMenuOpen(false);
setPlayersManagerOpen(true);
}}
@@ -908,7 +879,6 @@ export function EditorApp() {
if (!licenseActive) return;
setProjectMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setFileMenuOpen(false);
setPlaylistsManagerOpen(true);
}}
@@ -922,6 +892,19 @@ export function EditorApp() {
{appVersionText}
</div>
) : null}
<Button
variant="ghost"
iconOnly
tooltipPlacement="bottom"
title={t('control.helpTool')}
ariaLabel={t('control.helpTool')}
data-testid="editor-help-tool"
onClick={() => openInstructions('overview')}
>
<span className={styles.helpMark} aria-hidden>
?
</span>
</Button>
<div className={styles.headerActions}>
{state.project ? (
<>
@@ -1477,40 +1460,6 @@ export function EditorApp() {
snapshot={licenseSnap}
/>
<AppAboutModal open={appAboutOpen} onClose={() => setAppAboutOpen(false)} appVersion={appVersionText} />
{aboutMenuOpen && aboutMenuPos
? createPortal(
<div
role="menu"
data-aboutmenu-root="1"
className={styles.fileMenu}
style={{ left: aboutMenuPos.left, top: aboutMenuPos.top }}
>
<button
type="button"
role="menuitem"
className={styles.fileMenuItem}
onClick={() => {
setAboutMenuOpen(false);
setAppAboutOpen(true);
}}
>
{t('menu.aboutProgram')}
</button>
<button
type="button"
role="menuitem"
className={styles.fileMenuItem}
onClick={() => {
setAboutMenuOpen(false);
openInstructions('overview');
}}
>
{t('menu.instructions')}
</button>
</div>,
document.body,
)
: null}
{projectMenuOpen && projectMenuPos
? createPortal(
<div
@@ -2817,7 +2766,7 @@ function SceneInspector({
void getDndApi().invoke(ipcChannels.windows.openSceneEditor, {});
}}
>
Редактор сцены
{t('sceneEditor.open')}
</Button>
</>
) : null}