Add export method dialog, extract shared Dialog primitive

Add export dialog with download/clipboard options and optional
undo/redo history inclusion (default off). Extract shared Dialog
component to ui/dialog.tsx, consolidating open/close lifecycle,
backdrop click, and escape key handling from all 6 dialog components.
Update spec to reflect export method dialog and optional history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-27 14:57:31 +01:00
parent 4969ed069b
commit 209df13c32
10 changed files with 218 additions and 193 deletions

View File

@@ -28,11 +28,13 @@ import { useLongPress } from "../hooks/use-long-press.js";
import { cn } from "../lib/utils.js";
import {
assembleExportBundle,
bundleToJson,
readImportFile,
triggerDownload,
validateImportBundle,
} from "../persistence/export-import.js";
import { D20Icon } from "./d20-icon.js";
import { ExportMethodDialog } from "./export-method-dialog.js";
import { ImportConfirmDialog } from "./import-confirm-prompt.js";
import { ImportMethodDialog } from "./import-method-dialog.js";
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map.js";
@@ -449,20 +451,38 @@ export function ActionBar({
const importFileRef = useRef<HTMLInputElement>(null);
const [importError, setImportError] = useState<string | null>(null);
const [showExportMethod, setShowExportMethod] = useState(false);
const [showImportMethod, setShowImportMethod] = useState(false);
const [showImportConfirm, setShowImportConfirm] = useState(false);
const pendingBundleRef = useRef<
import("@initiative/domain").ExportBundle | null
>(null);
const handleExport = useCallback(() => {
const bundle = assembleExportBundle(
encounter,
undoRedoState,
playerCharacters,
);
triggerDownload(bundle);
}, [encounter, undoRedoState, playerCharacters]);
const handleExportDownload = useCallback(
(includeHistory: boolean) => {
const bundle = assembleExportBundle(
encounter,
undoRedoState,
playerCharacters,
includeHistory,
);
triggerDownload(bundle);
},
[encounter, undoRedoState, playerCharacters],
);
const handleExportClipboard = useCallback(
(includeHistory: boolean) => {
const bundle = assembleExportBundle(
encounter,
undoRedoState,
playerCharacters,
includeHistory,
);
void navigator.clipboard.writeText(bundleToJson(bundle));
},
[encounter, undoRedoState, playerCharacters],
);
const applyImport = useCallback(
(bundle: import("@initiative/domain").ExportBundle) => {
@@ -536,7 +556,7 @@ export function ActionBar({
bestiaryLoaded,
onBulkImport: showBulkImport,
bulkImportDisabled: bulkImportState.status === "loading",
onExportEncounter: handleExport,
onExportEncounter: () => setShowExportMethod(true),
onImportEncounter: () => setShowImportMethod(true),
onOpenSettings,
});
@@ -633,6 +653,12 @@ export function ActionBar({
autoDismissMs={5000}
/>
)}
<ExportMethodDialog
open={showExportMethod}
onDownload={handleExportDownload}
onCopyToClipboard={handleExportClipboard}
onClose={() => setShowExportMethod(false)}
/>
<ImportMethodDialog
open={showImportMethod}
onSelectFile={() => importFileRef.current?.click()}