feat(editor): add About menu with app info and user guide

Add header menu with About and Instructions modals so GMs can read product details and step-by-step help for all app features.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-06-28 14:09:23 +08:00
parent a96e1f8465
commit 8eb88399a3
5 changed files with 577 additions and 0 deletions
+84
View File
@@ -27,6 +27,7 @@ import {
} from './graph/SceneGraph';
import { useEditorI18n } from './i18n/EditorI18nContext';
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
import { AppAboutModal, InstructionsModal } from './about/EditorAboutModals';
import type { ProjectNoticeCode } from './state/projectState';
import { useProjectState } from './state/projectState';
@@ -76,6 +77,9 @@ 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 [instructionsOpen, setInstructionsOpen] = useState(false);
const [renameOpen, setRenameOpen] = useState(false);
const [exportModalOpen, setExportModalOpen] = useState(false);
const [previewBusy, setPreviewBusy] = useState(false);
@@ -125,9 +129,11 @@ 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 scenes = useMemo<SceneCard[]>(() => {
const p = state.project;
if (!p) return [];
@@ -269,6 +275,26 @@ 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(() => {
let off: (() => void) | null = null;
void (async () => {
@@ -393,6 +419,7 @@ export function EditorApp() {
onClick={() => {
setFileMenuOpen(false);
setProjectMenuOpen(false);
setAboutMenuOpen(false);
setSettingsMenuOpen((v) => {
const next = !v;
if (next) setSettingsLangSubOpen(false);
@@ -402,6 +429,21 @@ 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);
}}
>
{t('top.aboutApp')}
</button>
<button
ref={projectMenuBtnRef}
type="button"
@@ -413,6 +455,7 @@ export function EditorApp() {
if (!licenseActive) return;
setFileMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setProjectMenuOpen((v) => !v);
}}
>
@@ -430,6 +473,7 @@ export function EditorApp() {
if (!licenseActive) return;
setProjectMenuOpen(false);
setSettingsMenuOpen(false);
setAboutMenuOpen(false);
setFileMenuOpen((v) => !v);
}}
>
@@ -749,6 +793,46 @@ export function EditorApp() {
onClose={() => setAboutLicenseOpen(false)}
snapshot={licenseSnap}
/>
<AppAboutModal
open={appAboutOpen}
onClose={() => setAppAboutOpen(false)}
appVersion={appVersionText}
/>
<InstructionsModal open={instructionsOpen} onClose={() => setInstructionsOpen(false)} />
{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);
setInstructionsOpen(true);
}}
>
{t('menu.instructions')}
</button>
</div>,
document.body,
)
: null}
{projectMenuOpen && projectMenuPos
? createPortal(
<div