de9190959c
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>
186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
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,
|
||
type HelpSectionId,
|
||
} 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;
|
||
initialSection?: HelpSectionId;
|
||
};
|
||
|
||
function InstructionsModalBody({
|
||
initialSection,
|
||
onClose,
|
||
}: {
|
||
initialSection: HelpSectionId;
|
||
onClose: () => void;
|
||
}) {
|
||
const { t } = useEditorI18n();
|
||
const [activeId, setActiveId] = useState<HelpSectionId>(initialSection);
|
||
|
||
useEffect(() => {
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') onClose();
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
return () => window.removeEventListener('keydown', onKey);
|
||
}, [onClose]);
|
||
|
||
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,
|
||
);
|
||
}
|
||
|
||
export function InstructionsModal({ open, onClose, initialSection }: InstructionsModalProps) {
|
||
if (!open) return null;
|
||
const section = initialSection ?? 'overview';
|
||
return <InstructionsModalBody key={section} initialSection={section} onClose={onClose} />;
|
||
}
|