Files
ttrpg-tools/src/components/DataSourceDialog.tsx
T
hyper 53c8ce0677 feat(viewer): Add data source dialog
Introduce a dialog for switching between built-in content and a
user-selected local folder. Persist the folder handle in IndexedDB
so the selection survives page refreshes. Scan the folder for .md,
.yarn, and .csv files to build the file index.

Update App and Sidebar to pass file tree and heading data, and
add TypeScript declarations for the File System Access API.
2026-06-30 19:20:08 +08:00

160 lines
5.0 KiB
TypeScript

import { Component, createSignal, Show } from "solid-js";
import {
loadFromUserFolder,
switchToBuiltInSource,
getActiveSource,
clearIndex,
} from "../data-loader/file-index";
export interface DataSourceDialogProps {
isOpen: boolean;
onClose: () => void;
onSourceChanged: () => void;
}
/**
* Dialog for choosing how to source content files.
* Supports:
* - Built-in content (CLI / webpack bundled files)
* - Local folder (via File System Access API)
*/
export const DataSourceDialog: Component<DataSourceDialogProps> = (props) => {
const [loading, setLoading] = createSignal(false);
const [error, setError] = createSignal<string | null>(null);
const [folderPath, setFolderPath] = createSignal<string | null>(null);
const currentSource = () => getActiveSource();
const handleFolderPick = async () => {
setLoading(true);
setError(null);
const paths = await loadFromUserFolder();
if (paths === null) {
// User cancelled or API not supported — don't show error for cancellation
setLoading(false);
return;
}
if (paths.length === 0) {
setError(
"No supported files (.md, .yarn, .csv) found in the selected folder.",
);
setLoading(false);
return;
}
setFolderPath(`${paths.length} files loaded`);
setLoading(false);
props.onSourceChanged();
props.onClose();
};
const handleBuiltIn = async () => {
setLoading(true);
setError(null);
await switchToBuiltInSource();
// Force re-index from built-in sources
clearIndex();
setLoading(false);
props.onSourceChanged();
props.onClose();
};
return (
<Show when={props.isOpen}>
<div
class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"
onClick={props.onClose}
>
<div
class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4 p-6"
onClick={(e) => e.stopPropagation()}
>
<h2 class="text-lg font-bold text-gray-900 mb-4">Content Source</h2>
<div class="space-y-3">
{/* Built-in option */}
<button
onClick={handleBuiltIn}
disabled={loading()}
class="w-full text-left p-4 rounded-lg border transition-colors"
classList={{
"border-blue-500 bg-blue-50":
currentSource() === "cli" || currentSource() === "webpack",
"border-gray-200 hover:border-gray-300":
currentSource() !== "cli" && currentSource() !== "webpack",
"opacity-50 cursor-not-allowed": loading(),
}}
>
<div class="font-medium text-gray-900">Built-in Content</div>
<div class="text-sm text-gray-500 mt-1">
Use the bundled content files shipped with the app.
</div>
<Show
when={
currentSource() === "cli" || currentSource() === "webpack"
}
>
<div class="text-xs text-blue-600 mt-1 font-medium">Active</div>
</Show>
</button>
{/* Folder picker option */}
<button
onClick={handleFolderPick}
disabled={
loading() ||
typeof window === "undefined" ||
!("showDirectoryPicker" in window)
}
class="w-full text-left p-4 rounded-lg border transition-colors"
classList={{
"border-blue-500 bg-blue-50": currentSource() === "folder",
"border-gray-200 hover:border-gray-300":
currentSource() !== "folder",
"opacity-50 cursor-not-allowed":
loading() || !("showDirectoryPicker" in window),
}}
>
<div class="font-medium text-gray-900">Local Folder</div>
<div class="text-sm text-gray-500 mt-1">
Pick a folder on your computer. Supports .md, .yarn, and .csv
files.
{"showDirectoryPicker" in window
? " Will be remembered across sessions."
: " (Not supported in this browser)"}
</div>
<Show when={currentSource() === "folder"}>
<div class="text-xs text-blue-600 mt-1 font-medium">
Active{folderPath() ? ` — ${folderPath()}` : ""}
</div>
</Show>
</button>
</div>
<Show when={error()}>
<div class="mt-4 p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{error()}
</div>
</Show>
<div class="mt-6 flex justify-end">
<button
onClick={props.onClose}
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-900 rounded hover:bg-gray-100"
>
Close
</button>
</div>
</div>
</div>
</Show>
);
};
export default DataSourceDialog;