ttrpg-tools/src/App.tsx

114 lines
3.6 KiB
TypeScript

import { Component, createEffect, createMemo, createSignal } from "solid-js";
import { useLocation } from "@solidjs/router";
// 导入组件以注册自定义元素
import "./components";
import {
Article,
MobileSidebar,
DesktopSidebar,
DocDialog,
DataSourceDialog,
} from "./components";
import { generateToc, type FileNode, type TocNode } from "./data-loader";
const App: Component = () => {
const location = useLocation();
const [isSidebarOpen, setIsSidebarOpen] = createSignal(false);
const [isDocOpen, setIsDocOpen] = createSignal(false);
const [isDataSourceOpen, setIsDataSourceOpen] = createSignal(false);
// Sidebar and TOC data — reload when source changes
const [fileTree, setFileTree] = createSignal<FileNode[]>([]);
const [pathHeadings, setPathHeadings] = createSignal<
Record<string, TocNode[]>
>({});
const [tocKey, setTocKey] = createSignal(0);
const loadToc = async () => {
const toc = await generateToc();
setFileTree(toc.fileTree);
setPathHeadings(toc.pathHeadings);
};
// Load TOC on mount and when source changes
createEffect(() => {
void tocKey();
void loadToc();
});
const handleSourceChanged = () => {
setTocKey((k) => k + 1);
};
const currentPath = createMemo(() => {
// 根据路由加载对应的 markdown 文件
let path = decodeURIComponent(location.pathname);
if (!path) path = "/content/";
if (path.endsWith("/")) path += "index";
if (!path.endsWith(".md")) path += ".md";
return path;
});
return (
<div class="min-h-screen bg-gray-50">
{/* 桌面端固定侧边栏 */}
<DesktopSidebar fileTree={fileTree()} pathHeadings={pathHeadings()} />
{/* 移动端抽屉式侧边栏 */}
<MobileSidebar
isOpen={isSidebarOpen()}
onClose={() => setIsSidebarOpen(false)}
fileTree={fileTree()}
pathHeadings={pathHeadings()}
/>
<header class="fixed top-0 left-0 right-0 bg-white shadow z-30">
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-4">
{/* 仅在移动端显示菜单按钮 */}
<button
onClick={() => setIsSidebarOpen(true)}
class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
title="目录"
>
</button>
<h1 class="text-2xl font-bold text-gray-900">TTRPG Tools</h1>
<div class="flex-1" />
<button
onClick={() => setIsDataSourceOpen(true)}
class="text-sm text-gray-500 hover:text-gray-700 px-2 py-1 rounded hover:bg-gray-100"
title="Content Source"
>
📂
</button>
<button
onClick={() => setIsDocOpen(true)}
class="w-8 h-8 rounded-full border border-gray-300 text-gray-500 hover:text-gray-700 hover:border-gray-400 flex items-center justify-center text-sm font-medium"
title="指令文档"
>
?
</button>
</div>
</header>
{/* fill th rest of the space*/}
<div class="fixed top-16 left-0 right-0 bottom-0 overflow-auto">
<main class="max-w-4xl mx-auto px-4 py-8 pt-4 md:ml-64 2xl:ml-auto flex justify-center items-center">
<Article
class="prose text-black prose-sm max-w-full flex-1"
src={currentPath()}
/>
</main>
</div>
<DocDialog isOpen={isDocOpen()} onClose={() => setIsDocOpen(false)} />
<DataSourceDialog
isOpen={isDataSourceOpen()}
onClose={() => setIsDataSourceOpen(false)}
onSourceChanged={handleSourceChanged}
/>
</div>
);
};
export default App;