feat(editor): add Run help hint and rename from project list
Show a help icon when Run is disabled, open the relevant instruction section on click, and position its tooltip below-left. Add Rename project to the home screen project card menu. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
type SceneGraphSceneCard,
|
||||
type SceneGraphUiStrings,
|
||||
} from './graph/SceneGraph';
|
||||
import type { HelpSectionId } from './help/helpSections';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
|
||||
import type { ProjectNoticeCode } from './state/projectState';
|
||||
@@ -88,6 +89,7 @@ export function EditorApp() {
|
||||
const [aboutMenuOpen, setAboutMenuOpen] = useState(false);
|
||||
const [appAboutOpen, setAppAboutOpen] = useState(false);
|
||||
const [instructionsOpen, setInstructionsOpen] = useState(false);
|
||||
const [instructionsSection, setInstructionsSection] = useState<HelpSectionId>('overview');
|
||||
const [renameOpen, setRenameOpen] = useState(false);
|
||||
const [exportModalOpen, setExportModalOpen] = useState(false);
|
||||
const [previewDialogSceneId, setPreviewDialogSceneId] = useState<SceneId | null>(null);
|
||||
@@ -112,6 +114,7 @@ export function EditorApp() {
|
||||
[t],
|
||||
);
|
||||
const [state, actions] = useProjectState(licenseActive, { onNotice: onProjectNotice });
|
||||
const renameFromPickerRef = useRef(false);
|
||||
const sceneCardById = useStableSceneCardById(state.project);
|
||||
const graphUi = useMemo<SceneGraphUiStrings>(
|
||||
() => ({
|
||||
@@ -200,12 +203,6 @@ export function EditorApp() {
|
||||
return campaignAudioRefs.map((r) => p.assets[r.assetId]).filter((a): a is MediaAsset => Boolean(a));
|
||||
}, [campaignAudioRefs, state.project]);
|
||||
|
||||
const graphStartSceneId = useMemo(() => {
|
||||
const p = state.project;
|
||||
if (!p) return null;
|
||||
const gn = p.sceneGraphNodes.find((n) => n.isStartScene);
|
||||
return gn?.sceneId ?? null;
|
||||
}, [state.project]);
|
||||
const graphStartGraphNodeId = useMemo(() => {
|
||||
const p = state.project;
|
||||
if (!p) return null;
|
||||
@@ -213,6 +210,10 @@ export function EditorApp() {
|
||||
return gn?.id ?? null;
|
||||
}, [state.project]);
|
||||
|
||||
const runDisabled = !licenseActive || !graphStartGraphNodeId;
|
||||
const runHelpSection: HelpSectionId = !licenseActive ? 'license' : 'graph';
|
||||
const runHelpTooltip = !licenseActive ? t('top.afterLicense') : t('top.setStartScene');
|
||||
|
||||
const launchFromGraphNode = useCallback(
|
||||
(graphNodeId: GraphNodeId) => {
|
||||
if (!licenseActive) return;
|
||||
@@ -509,23 +510,33 @@ export function EditorApp() {
|
||||
) : null}
|
||||
<div className={styles.headerActions}>
|
||||
{state.project ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!licenseActive || !graphStartGraphNodeId}
|
||||
title={
|
||||
!licenseActive
|
||||
? t('top.afterLicense')
|
||||
: graphStartSceneId
|
||||
? undefined
|
||||
: t('top.setStartScene')
|
||||
}
|
||||
onClick={() => {
|
||||
if (!licenseActive || !graphStartGraphNodeId) return;
|
||||
launchFromGraphNode(graphStartGraphNodeId);
|
||||
}}
|
||||
>
|
||||
{t('top.run')}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={runDisabled}
|
||||
onClick={() => {
|
||||
if (!licenseActive || !graphStartGraphNodeId) return;
|
||||
launchFromGraphNode(graphStartGraphNodeId);
|
||||
}}
|
||||
>
|
||||
{t('top.run')}
|
||||
</Button>
|
||||
{runDisabled ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
ariaLabel={t('top.runHelpAria')}
|
||||
title={runHelpTooltip}
|
||||
tooltipPlacement="bottom-left"
|
||||
onClick={() => {
|
||||
setInstructionsSection(runHelpSection);
|
||||
setInstructionsOpen(true);
|
||||
}}
|
||||
>
|
||||
?
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -565,6 +576,21 @@ export function EditorApp() {
|
||||
openingProjectId={state.openingProjectId}
|
||||
onCreate={actions.createProject}
|
||||
onOpen={actions.openProject}
|
||||
onRename={(id) => {
|
||||
renameFromPickerRef.current = true;
|
||||
void (async () => {
|
||||
try {
|
||||
await actions.openProject(id);
|
||||
setRenameOpen(true);
|
||||
} catch (e) {
|
||||
renameFromPickerRef.current = false;
|
||||
setAppNotice({
|
||||
title: t('common.error'),
|
||||
message: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
})();
|
||||
}}
|
||||
onDelete={actions.deleteProject}
|
||||
/>
|
||||
)}
|
||||
@@ -831,7 +857,11 @@ export function EditorApp() {
|
||||
snapshot={licenseSnap}
|
||||
/>
|
||||
<AppAboutModal open={appAboutOpen} onClose={() => setAppAboutOpen(false)} appVersion={appVersionText} />
|
||||
<InstructionsModal open={instructionsOpen} onClose={() => setInstructionsOpen(false)} />
|
||||
<InstructionsModal
|
||||
open={instructionsOpen}
|
||||
initialSection={instructionsSection}
|
||||
onClose={() => setInstructionsOpen(false)}
|
||||
/>
|
||||
{aboutMenuOpen && aboutMenuPos
|
||||
? createPortal(
|
||||
<div
|
||||
@@ -857,6 +887,7 @@ export function EditorApp() {
|
||||
className={styles.fileMenuItem}
|
||||
onClick={() => {
|
||||
setAboutMenuOpen(false);
|
||||
setInstructionsSection('overview');
|
||||
setInstructionsOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -943,7 +974,13 @@ export function EditorApp() {
|
||||
fileBaseNameInitial={currentFileBaseName}
|
||||
existingProjectNames={existingProjectNames}
|
||||
existingFileBaseNames={existingFileBaseNames}
|
||||
onClose={() => setRenameOpen(false)}
|
||||
onClose={() => {
|
||||
setRenameOpen(false);
|
||||
if (renameFromPickerRef.current) {
|
||||
renameFromPickerRef.current = false;
|
||||
void actions.closeProject();
|
||||
}
|
||||
}}
|
||||
onSave={async (name, fileBaseName) => {
|
||||
await actions.renameProject(name, fileBaseName);
|
||||
}}
|
||||
@@ -1538,6 +1575,7 @@ type ProjectPickerProps = {
|
||||
openingProjectId: ProjectId | null;
|
||||
onCreate: (name: string) => Promise<void>;
|
||||
onOpen: (id: ProjectId) => Promise<void>;
|
||||
onRename: (id: ProjectId) => void;
|
||||
onDelete: (id: ProjectId) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -1547,6 +1585,7 @@ function ProjectPicker({
|
||||
openingProjectId,
|
||||
onCreate,
|
||||
onOpen,
|
||||
onRename,
|
||||
onDelete,
|
||||
}: ProjectPickerProps) {
|
||||
const { t, locale } = useEditorI18n();
|
||||
@@ -1679,6 +1718,20 @@ function ProjectPicker({
|
||||
className={styles.fileMenu}
|
||||
style={{ left: rowMenuPos.left, top: rowMenuPos.top }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={styles.fileMenuItem}
|
||||
onClick={() => {
|
||||
const id = rowMenuFor;
|
||||
setRowMenuFor(null);
|
||||
setRowMenuPos(null);
|
||||
if (!id) return;
|
||||
onRename(id);
|
||||
}}
|
||||
>
|
||||
{t('fileMenu.rename')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
|
||||
Reference in New Issue
Block a user