Files
DndGamePlayer/app/renderer/editor/FoundryImportModal.tsx
T
Ivan Fontosh f270812219 feat(tokens): app-local non-player tokens with session moves and UI polish
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:04:12 +08:00

147 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { Button, Select } from '../shared/ui/controls';
import styles from './EditorApp.module.css';
import { useEditorI18n } from './i18n/EditorI18nContext';
export type FoundryImportSourceSelection =
| { kind: 'folder'; sourcePath: string }
| { kind: 'archive'; sourcePath: string };
type FoundryImportModalProps = {
open: boolean;
pickSource: (
mode: 'folder' | 'archive',
) => Promise<{ canceled: true } | { canceled: false; sourcePath: string }>;
onClose: () => void;
onImport: (selection: FoundryImportSourceSelection) => Promise<void>;
};
export function FoundryImportModal({ open, pickSource, onClose, onImport }: FoundryImportModalProps) {
const { t } = useEditorI18n();
const [mode, setMode] = useState<'folder' | 'archive'>('folder');
const [picked, setPicked] = useState<{ path: string; name: string } | null>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setMode('folder');
setPicked(null);
setSubmitting(false);
setError(null);
}, [open]);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !submitting) onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, open, submitting]);
if (!open) return null;
return createPortal(
<>
<button
type="button"
aria-label={t('common.close')}
onClick={() => {
if (!submitting) onClose();
}}
className={styles.modalBackdrop}
/>
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>{t('foundryImport.title')}</div>
<button
type="button"
aria-label={t('common.close')}
onClick={() => {
if (!submitting) onClose();
}}
className={styles.modalClose}
>
×
</button>
</div>
<div className={styles.fieldGrid}>
<div className={styles.muted}>{t('foundryImport.hint')}</div>
<div className={styles.fieldLabel}>{t('foundryImport.sourceType')}</div>
<Select
value={mode}
disabled={submitting}
ariaLabel={t('foundryImport.sourceType')}
options={[
{ value: 'folder', label: t('foundryImport.folder') },
{ value: 'archive', label: t('foundryImport.archive') },
]}
onChange={(next) => {
setMode(next as 'folder' | 'archive');
setPicked(null);
setError(null);
}}
/>
<div className={styles.fieldLabel}>{t('foundryImport.source')}</div>
<div className={styles.importFileRow}>
<Button
disabled={submitting}
onClick={() => {
void (async () => {
setError(null);
const res = await pickSource(mode);
if (res.canceled) return;
const name = res.sourcePath.split(/[/\\]/).pop() ?? res.sourcePath;
setPicked({ path: res.sourcePath, name });
})();
}}
>
{mode === 'folder' ? t('foundryImport.chooseFolder') : t('foundryImport.chooseArchive')}
</Button>
<span className={styles.muted}>{picked ? picked.name : t('foundryImport.noSourceSelected')}</span>
</div>
</div>
{error ? <div className={styles.fieldError}>{error}</div> : null}
<div className={styles.modalFooter}>
<Button onClick={onClose} disabled={submitting}>
{t('common.cancel')}
</Button>
<Button
variant="primary"
disabled={!picked || submitting}
onClick={() => {
if (!picked) return;
void (async () => {
setSubmitting(true);
setError(null);
try {
await onImport({
kind: mode,
sourcePath: picked.path,
});
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setSubmitting(false);
}
})();
}}
>
{t('foundryImport.import')}
</Button>
</div>
</div>
</>,
document.body,
);
}