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
+17 -15
View File
@@ -12,6 +12,7 @@ import {
} from '../editor/fileDrop';
import editorStyles from '../editor/EditorApp.module.css';
import matStyles from '../editor/MaterialsModals.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi';
import { Button, Input } from '../shared/ui/controls';
import { useTokenImageUrl } from '../shared/tokens/useTokenImageUrl';
@@ -29,6 +30,7 @@ function normalizeName(input: string): string {
}
export function TokenEditModal({ open, initial, existingNames, onClose, onSaved }: Props) {
const { t } = useEditorI18n();
const api = getDndApi();
const [name, setName] = useState('');
const [filePath, setFilePath] = useState<string | null>(null);
@@ -94,26 +96,26 @@ export function TokenEditModal({ open, initial, existingNames, onClose, onSaved
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={editorStyles.modalBackdrop} />
<button type="button" aria-label={t('common.close')} onClick={onClose} className={editorStyles.modalBackdrop} />
<div role="dialog" aria-modal="true" className={editorStyles.modalDialog}>
<div className={editorStyles.modalHeader}>
<div className={editorStyles.modalTitle}>
{initial ? 'Изменить токен' : 'Добавить токен'}
{initial ? t('sceneEditor.editToken') : t('sceneEditor.addToken')}
</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={editorStyles.modalClose}>
<button type="button" aria-label={t('common.close')} onClick={onClose} className={editorStyles.modalClose}>
×
</button>
</div>
<div className={editorStyles.fieldGrid}>
<div className={editorStyles.fieldLabel}>Название</div>
<Input value={name} onChange={setName} placeholder="Название токена" />
{!nameOk ? <div className={editorStyles.fieldError}>Укажите название</div> : null}
{nameDup ? <div className={editorStyles.fieldError}>Такое название уже есть</div> : null}
<div className={editorStyles.fieldLabel}>{t('sceneEditor.tokenName')}</div>
<Input value={name} onChange={setName} placeholder={t('sceneEditor.tokenNamePlaceholder')} />
{!nameOk ? <div className={editorStyles.fieldError}>{t('sceneEditor.tokenNameRequired')}</div> : null}
{nameDup ? <div className={editorStyles.fieldError}>{t('sceneEditor.tokenNameDuplicate')}</div> : null}
</div>
<div className={editorStyles.fieldGrid}>
<div className={editorStyles.fieldLabel}>Изображение</div>
<div className={editorStyles.fieldLabel}>{t('sceneEditor.image')}</div>
<div
className={[matStyles.imageDrop, drop.dragOver ? matStyles.imageDropOver : ''].join(' ')}
onDragEnter={drop.onDragEnter}
@@ -137,12 +139,12 @@ export function TokenEditModal({ open, initial, existingNames, onClose, onSaved
}}
>
{drop.dragOver ? (
<div className={editorStyles.dropHintOverlay}>Отпустите, чтобы загрузить изображение</div>
<div className={editorStyles.dropHintOverlay}>{t('sceneEditor.dropImage')}</div>
) : null}
{previewSrc ? (
<img src={previewSrc} alt="" className={matStyles.previewThumb} draggable={false} />
) : (
<div className={editorStyles.muted}>Перетащите изображение или выберите файл</div>
<div className={editorStyles.muted}>{t('sceneEditor.dropOrPick')}</div>
)}
<Button
onClick={() => {
@@ -153,17 +155,17 @@ export function TokenEditModal({ open, initial, existingNames, onClose, onSaved
})();
}}
>
Выбрать изображение
{t('sceneEditor.chooseImage')}
</Button>
</div>
{!hasImage ? <div className={editorStyles.fieldError}>Нужно изображение</div> : null}
{!hasImage ? <div className={editorStyles.fieldError}>{t('sceneEditor.imageRequired')}</div> : null}
</div>
{error ? <div className={editorStyles.fieldError}>{error}</div> : null}
<div className={editorStyles.modalFooter}>
<Button onClick={onClose} disabled={saving}>
Отмена
{t('common.cancel')}
</Button>
<Button
variant="primary"
@@ -182,14 +184,14 @@ export function TokenEditModal({ open, initial, existingNames, onClose, onSaved
onSaved(token);
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : 'Не удалось сохранить');
setError(e instanceof Error ? e.message : t('sceneEditor.saveFailed'));
} finally {
setSaving(false);
}
})();
}}
>
Сохранить
{t('common.save')}
</Button>
</div>
</div>