ttrpg-tools/src/components/md-deck/editor-panel/LayerEditorPanel.tsx

152 lines
4.9 KiB
TypeScript

import { For, createSignal, createMemo, onCleanup, onMount } from "solid-js";
import {
DragDropProvider,
DragDropSensors,
SortableProvider,
useDragDropContext,
closestCenter,
} from "@thisbeyond/solid-dnd";
import type { DeckStore } from "../hooks/deckStore";
import { LayerRow } from "./LayerRow";
export interface LayerEditorPanelProps {
store: DeckStore;
}
function LayerEditorPanel(props: LayerEditorPanelProps) {
const { store } = props;
const [openDropdown, setOpenDropdown] = createSignal<string | null>(null);
const [hoveredIndex, setHoveredIndex] = createSignal<number | null>(null);
const [addMenuOpen, setAddMenuOpen] = createSignal(false);
let addMenuRef: HTMLDivElement | undefined;
const side = () => store.state.activeSide;
const layers = () =>
side() === "front"
? store.state.frontLayerConfigs
: store.state.backLayerConfigs;
// Stable IDs for SortableProvider — simply the indices
const ids = createMemo(() => layers().map((_, i) => i));
const updater = () =>
side() === "front"
? store.actions.updateFrontLayerConfig
: store.actions.updateBackLayerConfig;
const availableFields = createMemo(() => {
const cards = store.state.cards;
if (!cards?.length) return [];
return Object.keys(cards[0]).filter((k) => k !== "label");
});
const update = (
index: number,
patch: Partial<import("../types").LayerConfig>,
closeDropdown = false,
) => {
updater()(index, patch);
if (closeDropdown) setOpenDropdown(null);
};
onMount(() => {
const h = (e: MouseEvent) => {
if (addMenuRef && !addMenuRef.contains(e.target as Node))
setAddMenuOpen(false);
};
document.addEventListener("click", h);
onCleanup(() => document.removeEventListener("click", h));
});
return (
<div class="w-64 shrink-0">
<h3 class="font-bold mb-2 mt-0">
({side() === "front" ? "正面" : "背面"})
</h3>
<div class="relative mb-2" ref={addMenuRef}>
<button
onClick={() => setAddMenuOpen(!addMenuOpen())}
class="w-full flex items-center gap-2 px-3 py-1.5 text-sm rounded cursor-pointer border border-dashed border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-600"
>
<span class="text-lg leading-none">+</span>
<span></span>
</button>
{addMenuOpen() && (
<div
class="absolute top-full left-0 mt-1 w-full bg-white border border-gray-300 rounded shadow-lg z-20 max-h-48 overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<For each={availableFields()}>
{(f) => (
<button
onClick={() => {
store.actions.addLayer(side(), f);
setAddMenuOpen(false);
}}
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-blue-50 hover:text-blue-600 cursor-pointer"
>
{f}
</button>
)}
</For>
</div>
)}
</div>
<DragDropProvider
onDragEnd={({ draggable, droppable }) => {
if (droppable && draggable.id !== droppable.id) {
store.actions.reorderLayers(
side(),
draggable.id as number,
droppable.id as number,
);
}
}}
collisionDetector={closestCenter}
>
<DragDropSensors>
<SortableProvider ids={ids()}>
<For each={layers()}>
{(layer, index) => (
<LayerRow
store={store}
index={index()}
layer={layer}
hovered={hoveredIndex() === index()}
onHoverChange={setHoveredIndex}
openDropdown={openDropdown()}
setOpenDropdown={setOpenDropdown}
onUpdateOrientation={(o) =>
update(index(), { orientation: o }, true)
}
onUpdateFontSize={(fs) => update(index(), { fontSize: fs })}
onUpdateAlign={(a) => update(index(), { align: a }, true)}
onSelect={() =>
store.actions.setSelectedLayer(
store.state.selectedLayer === index() ? null : index(),
)
}
onRemove={() => store.actions.removeLayer(side(), index())}
/>
)}
</For>
</SortableProvider>
</DragDropSensors>
</DragDropProvider>
<hr class="my-4" />
<button
onClick={() => store.actions.copyCode()}
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium cursor-pointer flex items-center gap-2 justify-center"
>
<span>📋</span>
<span></span>
</button>
</div>
);
}
export { LayerEditorPanel };