Files
Genarrative/src/components/visual-novel-runtime/VisualNovelRuntimePanels.tsx
2026-05-08 11:44:42 +08:00

143 lines
4.6 KiB
TypeScript

import { BarChart3, Bookmark, History, Settings, X } from 'lucide-react';
import { createPortal } from 'react-dom';
import type { ProfileSaveArchiveSummary } from '../../../packages/shared/src/contracts/runtime';
import type {
VisualNovelResultDraft,
VisualNovelRunSnapshot,
} from '../../../packages/shared/src/contracts/visualNovel';
import { VisualNovelAttributePanel } from './VisualNovelAttributePanel';
import { VisualNovelHistoryPanel } from './VisualNovelHistoryPanel';
import { VisualNovelSavePanel } from './VisualNovelSavePanel';
import { VisualNovelSettingsPanel } from './VisualNovelSettingsPanel';
export type VisualNovelRuntimePanelKind =
| 'history'
| 'save'
| 'settings'
| 'attributes';
type VisualNovelRuntimePanelProps = {
kind: VisualNovelRuntimePanelKind;
draft: VisualNovelResultDraft;
run: VisualNovelRunSnapshot;
isBusy?: boolean;
isSaving?: boolean;
isLoadingArchives?: boolean;
resumingWorldKey?: string | null;
saveArchives?: ProfileSaveArchiveSummary[];
onClose: () => void;
onRegenerateHistoryEntry?: (entryId: string) => void;
onSaveRun?: () => void;
onResumeSaveArchive?: (worldKey: string) => void;
textModeEnabled?: boolean;
onTextModeChange?: (enabled: boolean) => void;
allowRegeneration?: boolean;
};
const PANEL_META: Record<
VisualNovelRuntimePanelKind,
{ title: string; icon: typeof History }
> = {
history: { title: '历史', icon: History },
save: { title: '存档', icon: Bookmark },
settings: { title: '设置', icon: Settings },
attributes: { title: '属性', icon: BarChart3 },
};
export function VisualNovelRuntimePanel({
kind,
draft,
run,
isBusy = false,
isSaving = false,
isLoadingArchives = false,
resumingWorldKey = null,
saveArchives = [],
onClose,
onRegenerateHistoryEntry,
onSaveRun,
onResumeSaveArchive,
textModeEnabled = false,
onTextModeChange,
allowRegeneration = false,
}: VisualNovelRuntimePanelProps) {
if (typeof document === 'undefined') {
return null;
}
const meta = PANEL_META[kind];
const Icon = meta.icon;
return createPortal(
<div
className="platform-overlay fixed inset-0 z-[150] flex items-end justify-center p-3 backdrop-blur-sm sm:items-center sm:p-4"
onClick={(event) => {
if (event.target === event.currentTarget) {
onClose();
}
}}
>
<section
role="dialog"
aria-modal="true"
aria-label={meta.title}
className="platform-modal-shell platform-remap-surface flex max-h-[min(84vh,40rem)] w-full max-w-lg flex-col overflow-hidden rounded-t-[1.45rem] sm:rounded-[1.45rem]"
onClick={(event) => event.stopPropagation()}
>
<header className="flex items-center justify-between gap-3 border-b border-[var(--platform-subpanel-border)] px-4 py-3">
<div className="flex min-w-0 items-center gap-2">
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-[var(--platform-neutral-bg)] text-[var(--platform-neutral-text)]">
<Icon className="h-4 w-4" />
</span>
<h2 className="truncate text-base font-black text-[var(--platform-text-strong)]">
{meta.title}
</h2>
</div>
<button
type="button"
className="platform-icon-button h-9 w-9"
onClick={onClose}
aria-label="关闭"
title="关闭"
>
<X className="h-4 w-4" />
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
{kind === 'history' ? (
<VisualNovelHistoryPanel
run={run}
allowRegeneration={allowRegeneration}
isBusy={isBusy}
onRegenerateHistoryEntry={onRegenerateHistoryEntry}
/>
) : null}
{kind === 'save' ? (
<VisualNovelSavePanel
run={run}
saveArchives={saveArchives}
isSaving={isSaving}
isLoadingArchives={isLoadingArchives}
resumingWorldKey={resumingWorldKey}
onSaveRun={onSaveRun}
onResumeSaveArchive={onResumeSaveArchive}
/>
) : null}
{kind === 'settings' ? (
<VisualNovelSettingsPanel
draft={draft}
textModeEnabled={textModeEnabled}
onTextModeChange={onTextModeChange}
/>
) : null}
{kind === 'attributes' ? <VisualNovelAttributePanel run={run} /> : null}
</div>
</section>
</div>,
document.body,
);
}
export default VisualNovelRuntimePanel;