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:
@@ -0,0 +1,177 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { APP_DISPLAY_NAME_EN, APP_DISPLAY_NAME_RU } from '../../../shared/appBranding';
|
||||
import { Button } from '../../shared/ui/controls';
|
||||
import styles from '../EditorApp.module.css';
|
||||
import { HELP_SECTION_IDS, helpSectionBodyKey, helpSectionTitleKey } from '../help/helpSections';
|
||||
import { useEditorI18n } from '../i18n/EditorI18nContext';
|
||||
|
||||
type AppAboutModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
appVersion: string | null;
|
||||
};
|
||||
|
||||
export function AppAboutModal({ open, onClose, appVersion }: AppAboutModalProps) {
|
||||
const { t, locale } = useEditorI18n();
|
||||
const appName = locale === 'ru' ? APP_DISPLAY_NAME_RU : APP_DISPLAY_NAME_EN;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose, open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
onClick={onClose}
|
||||
className={styles.modalBackdrop}
|
||||
/>
|
||||
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
|
||||
<div className={styles.modalHeader}>
|
||||
<div className={styles.modalTitle}>{t('app.about.title')}</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
onClick={onClose}
|
||||
className={styles.modalClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.aboutBody}>
|
||||
<div className={styles.aboutAppName}>{appName}</div>
|
||||
<div className={styles.aboutTagline}>{t('app.about.tagline')}</div>
|
||||
<p className={styles.aboutParagraph}>{t('app.about.description')}</p>
|
||||
<div className={styles.aboutMetaGrid}>
|
||||
<div className={styles.fieldLabel}>{t('app.about.versionLabel')}</div>
|
||||
<div>{appVersion ?? '—'}</div>
|
||||
<div className={styles.fieldLabel}>{t('app.about.developerLabel')}</div>
|
||||
<div>{t('app.about.developer')}</div>
|
||||
<div className={styles.fieldLabel}>{t('app.about.supportLabel')}</div>
|
||||
<div>
|
||||
<a className={styles.aboutLink} href={`mailto:${t('app.about.supportEmail')}`}>
|
||||
{t('app.about.supportEmail')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.fieldLabel}>{t('app.about.websiteLabel')}</div>
|
||||
<div>
|
||||
<a
|
||||
className={styles.aboutLink}
|
||||
href={t('app.about.websiteUrl')}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('app.about.websiteUrl')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
type InstructionsModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function InstructionsModal({ open, onClose }: InstructionsModalProps) {
|
||||
const { t } = useEditorI18n();
|
||||
const [activeId, setActiveId] = useState<(typeof HELP_SECTION_IDS)[number]>('overview');
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setActiveId('overview');
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [onClose, open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const body = t(helpSectionBodyKey(activeId));
|
||||
const paragraphs = body.split('\n\n').filter((p) => p.trim() !== '');
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
onClick={onClose}
|
||||
className={styles.modalBackdrop}
|
||||
/>
|
||||
<div role="dialog" aria-modal="true" className={styles.instructionsDialog}>
|
||||
<div className={styles.modalHeader}>
|
||||
<div className={styles.modalTitle}>{t('help.title')}</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('common.close')}
|
||||
onClick={onClose}
|
||||
className={styles.modalClose}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.instructionsLayout}>
|
||||
<div className={styles.instructionsContent}>
|
||||
<div className={styles.instructionsContentTitle}>
|
||||
{t(helpSectionTitleKey(activeId))}
|
||||
</div>
|
||||
{paragraphs.map((p, i) => (
|
||||
<p key={i} className={styles.instructionsParagraph}>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<nav className={styles.instructionsNav} aria-label={t('help.navAria')}>
|
||||
{HELP_SECTION_IDS.map((id) => (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={[
|
||||
styles.instructionsNavItem,
|
||||
id === activeId ? styles.instructionsNavItemActive : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
aria-current={id === activeId ? 'true' : undefined}
|
||||
onClick={() => setActiveId(id)}
|
||||
>
|
||||
{t(helpSectionTitleKey(id))}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user