Files
ttrpg-tools/src/components/md-deck/index.tsx
T
hyper 6bcdca3a77 feat(md-deck): Add card list navigation and tabbed editor panels
- Move card tab selector to a left sidebar (CardList) with pagination
- Replace separate side toggle and layer panels with EditorTabs
- Move copy code button from layer panel to DeckHeader
- Simplify PropertiesEditorPanel by removing front/back toggle and
  using a select for shape
2026-08-07 11:20:26 +08:00

159 lines
4.5 KiB
TypeScript

import { customElement, noShadowDOM } from "solid-element";
import { Show, onCleanup } from "solid-js";
import { resolvePath } from "../utils/path";
import { v4 as uuidv4 } from "uuid";
import { createDeckStore } from "./hooks/deckStore";
import { registerDeck, unregisterDeck } from "./hooks/deck-registry";
import type { CardShape } from "./types";
import { DeckHeader } from "./DeckHeader";
import { CardList } from "./CardList";
import { DeckContent } from "./DeckContent";
import { EditorTabs } from "./EditorTabs";
import { PrintPreview } from "./PrintPreview";
import { DataEditorPanel } from "./editor-panel";
interface DeckProps {
size?: string;
sizeW?: number;
sizeH?: number;
grid?: string;
gridW?: number;
gridH?: number;
bleed?: number | string;
padding?: number | string;
shape?: CardShape;
layers?: string;
backLayers?: string;
fixed?: boolean | string;
}
customElement<DeckProps>(
"md-deck",
{
size: "",
sizeW: 54,
sizeH: 86,
grid: "",
gridW: 5,
gridH: 8,
bleed: 1,
padding: 2,
shape: "rectangle",
layers: "",
backLayers: "",
fixed: false,
},
(props, { element }) => {
noShadowDOM();
// 从 element 的 textContent 获取 CSV 路径
const csvPath = element?.textContent?.trim() || "";
// 隐藏原始文本内容
if (element) {
element.textContent = "";
}
// 从父节点 article 的 data-src 获取当前 markdown 文件完整路径
const articleEl = element?.closest("article[data-src]");
const articlePath = articleEl?.getAttribute("data-src") || "";
// 解析相对路径
const resolvedSrc = resolvePath(articlePath, csvPath);
// 创建 store 并加载数据
const store = createDeckStore(resolvedSrc);
// 生成唯一 ID 并注册到全局注册表
const deckId = `deck-${uuidv4()}`;
registerDeck(deckId, store, resolvedSrc, csvPath);
// 解析 size 属性(支持旧格式 "54x86" 和新格式)
if (props.size && props.size.includes("x")) {
const [w, h] = props.size.split("x").map(Number);
store.actions.setSizeW(w);
store.actions.setSizeH(h);
} else {
store.actions.setSizeW(props.sizeW ?? 54);
store.actions.setSizeH(props.sizeH ?? 86);
}
// 解析 grid 属性(支持旧格式 "5x8" 和新格式)
if (props.grid && props.grid.includes("x")) {
const [w, h] = props.grid.split("x").map(Number);
store.actions.setGridW(w);
store.actions.setGridH(h);
} else {
store.actions.setGridW(props.gridW ?? 5);
store.actions.setGridH(props.gridH ?? 8);
}
// 解析 bleed 和 padding(支持旧字符串格式和新数字格式)
if (typeof props.bleed === "string") {
store.actions.setBleed(Number(props.bleed));
} else {
store.actions.setBleed(props.bleed ?? 1);
}
if (typeof props.padding === "string") {
store.actions.setPadding(Number(props.padding));
} else {
store.actions.setPadding(props.padding ?? 2);
}
// 设置形状
store.actions.setShape(props.shape ?? "rectangle");
// 加载 CSV 数据
store.actions.loadCardsFromPath(
resolvedSrc,
csvPath,
(props.layers as string) || "",
(props.backLayers as string) || "",
);
// 清理函数
onCleanup(() => {
unregisterDeck(deckId);
store.actions.clearError();
});
return (
<div class="md-deck mb-4">
{/* 导出 PDF 预览弹窗 */}
<Show when={store.state.isExporting}>
<PrintPreview
articlePath={articlePath}
store={store}
deckId={deckId}
onClose={() => store.actions.setExporting(false)}
onExport={() => {}}
/>
</Show>
{/* Tab 选择器和编辑按钮 */}
<Show when={store.state.cards.length > 0 && !store.state.error}>
<DeckHeader store={store} />
</Show>
<div class="flex gap-4">
{/* 左侧:卡牌列表导航 */}
<Show when={store.state.cards.length > 0 && !store.state.error}>
<CardList store={store} />
</Show>
{/* 中间:内容区域(错误/加载/卡牌预览/空状态) */}
<div class="flex-1 min-w-0">
<DeckContent store={store} isLoading={store.state.isLoading} />
</div>
{/* 右侧:属性/图层编辑面板(标签页切换) */}
<Show when={store.state.isEditing && !store.state.fixed}>
<EditorTabs store={store} />
</Show>
</div>
</div>
);
},
);