a29c72b307
Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию. Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
import { ipcChannels } from '../../shared/ipc/contracts';
|
|
import { buildHelpLinkCatalog, splitHelpTextWithLinks } from '../editor/help/helpLinkify';
|
|
import {
|
|
HELP_SECTION_IDS,
|
|
helpSectionBodyKey,
|
|
helpSectionTitleKey,
|
|
parseHelpSectionId,
|
|
type HelpSectionId,
|
|
} from '../editor/help/helpSections';
|
|
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
|
import { getDndApi } from '../shared/dndApi';
|
|
|
|
import styles from './InstructionsApp.module.css';
|
|
|
|
function InstructionsPanel({ initialSection }: { initialSection: HelpSectionId }) {
|
|
const { t } = useEditorI18n();
|
|
const [activeId, setActiveId] = useState<HelpSectionId>(initialSection);
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
|
const navRef = useRef<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
setActiveId(initialSection);
|
|
}, [initialSection]);
|
|
|
|
useEffect(() => {
|
|
contentRef.current?.scrollTo({ top: 0 });
|
|
const activeNav = navRef.current?.querySelector<HTMLElement>('[aria-current="true"]');
|
|
activeNav?.scrollIntoView({ block: 'nearest' });
|
|
}, [activeId]);
|
|
|
|
const linkCatalog = useMemo(
|
|
() => buildHelpLinkCatalog((id) => t(helpSectionTitleKey(id))),
|
|
[t],
|
|
);
|
|
|
|
const body = t(helpSectionBodyKey(activeId));
|
|
const paragraphs = body.split('\n\n').filter((p) => p.trim() !== '');
|
|
|
|
return (
|
|
<div className={styles.page} data-testid="instructions-app">
|
|
<div ref={contentRef} className={styles.content}>
|
|
<div className={styles.contentTitle}>{t(helpSectionTitleKey(activeId))}</div>
|
|
{paragraphs.map((p, i) => (
|
|
<p key={i} className={styles.paragraph}>
|
|
{splitHelpTextWithLinks(p, linkCatalog).map((part, j) =>
|
|
part.type === 'text' ? (
|
|
<React.Fragment key={j}>{part.value}</React.Fragment>
|
|
) : (
|
|
<button
|
|
key={j}
|
|
type="button"
|
|
className={styles.inlineLink}
|
|
onClick={() => setActiveId(part.id)}
|
|
>
|
|
{part.value}
|
|
</button>
|
|
),
|
|
)}
|
|
</p>
|
|
))}
|
|
</div>
|
|
<nav ref={navRef} className={styles.nav} aria-label={t('help.navAria')}>
|
|
{HELP_SECTION_IDS.map((id) => (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
className={[styles.navItem, id === activeId ? styles.navItemActive : '']
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
aria-current={id === activeId ? 'true' : undefined}
|
|
onClick={() => setActiveId(id)}
|
|
>
|
|
{t(helpSectionTitleKey(id))}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function InstructionsApp() {
|
|
const [section, setSection] = useState<HelpSectionId>('overview');
|
|
|
|
useEffect(() => {
|
|
const api = getDndApi();
|
|
void api.invoke(ipcChannels.windows.getInstructionsSection, {}).then(({ section: next }) => {
|
|
setSection(parseHelpSectionId(next));
|
|
});
|
|
return api.on(ipcChannels.windows.instructionsSection, ({ section: next }) => {
|
|
setSection(parseHelpSectionId(next));
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') window.close();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, []);
|
|
|
|
return <InstructionsPanel initialSection={section} />;
|
|
}
|