2026-06-30 20:55:13 +08:00
|
|
|
import { For, createSignal, createMemo, onCleanup, onMount } from "solid-js";
|
|
|
|
|
import type { DeckStore } from "../hooks/deckStore";
|
|
|
|
|
import {
|
|
|
|
|
DragDropProvider,
|
|
|
|
|
DragDropSensors,
|
|
|
|
|
createSortable,
|
|
|
|
|
maybeTransformStyle,
|
|
|
|
|
closestCenter,
|
|
|
|
|
} from "@thisbeyond/solid-dnd";
|
|
|
|
|
import alignLeftIcon from "./icons/align-left.png";
|
|
|
|
|
import alignCenterIcon from "./icons/align-center.png";
|
|
|
|
|
import alignRightIcon from "./icons/align-right.png";
|
2026-02-27 15:33:23 +08:00
|
|
|
|
|
|
|
|
export interface LayerEditorPanelProps {
|
|
|
|
|
store: DeckStore;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 22:56:06 +08:00
|
|
|
const ORIENTATION_OPTIONS = [
|
2026-06-30 20:55:13 +08:00
|
|
|
{ value: "n", label: "↑ 北" },
|
|
|
|
|
{ value: "e", label: "→ 东" },
|
|
|
|
|
{ value: "s", label: "↓ 南" },
|
|
|
|
|
{ value: "w", label: "← 西" },
|
2026-02-27 22:56:06 +08:00
|
|
|
] as const;
|
|
|
|
|
|
2026-03-27 15:17:25 +08:00
|
|
|
const ALIGN_OPTIONS = [
|
2026-06-30 20:55:13 +08:00
|
|
|
{ value: "", label: "默认", icon: alignCenterIcon },
|
|
|
|
|
{ value: "l", label: "左对齐", icon: alignLeftIcon },
|
|
|
|
|
{ value: "c", label: "居中", icon: alignCenterIcon },
|
|
|
|
|
{ value: "r", label: "右对齐", icon: alignRightIcon },
|
2026-03-27 15:17:25 +08:00
|
|
|
] as const;
|
|
|
|
|
|
2026-03-30 11:40:45 +08:00
|
|
|
const FONT_PRESETS = [3, 5, 8, 12] as const;
|
|
|
|
|
|
|
|
|
|
function OrientationIcon(value: string): string {
|
|
|
|
|
switch (value) {
|
2026-06-30 20:55:13 +08:00
|
|
|
case "n":
|
|
|
|
|
return "↑";
|
|
|
|
|
case "e":
|
|
|
|
|
return "→";
|
|
|
|
|
case "s":
|
|
|
|
|
return "↓";
|
|
|
|
|
case "w":
|
|
|
|
|
return "←";
|
|
|
|
|
default:
|
|
|
|
|
return "↑";
|
2026-03-30 11:40:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 12:54:53 +08:00
|
|
|
function AlignIconSrc(value: string): string {
|
2026-03-30 11:40:45 +08:00
|
|
|
switch (value) {
|
2026-06-30 20:55:13 +08:00
|
|
|
case "l":
|
|
|
|
|
return alignLeftIcon;
|
|
|
|
|
case "r":
|
|
|
|
|
return alignRightIcon;
|
|
|
|
|
default:
|
|
|
|
|
return alignCenterIcon;
|
2026-03-30 11:40:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LayerEditorPanel(props: LayerEditorPanelProps) {
|
2026-02-27 15:33:23 +08:00
|
|
|
const { store } = props;
|
2026-03-30 11:40:45 +08:00
|
|
|
const [openDropdown, setOpenDropdown] = createSignal<string | null>(null);
|
2026-06-30 20:55:13 +08:00
|
|
|
const [hoveredIndex, setHoveredIndex] = createSignal<number | null>(null);
|
|
|
|
|
const [addMenuOpen, setAddMenuOpen] = createSignal(false);
|
|
|
|
|
let addMenuRef: HTMLDivElement | undefined;
|
2026-02-27 15:33:23 +08:00
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
const side = () => store.state.activeSide;
|
|
|
|
|
|
|
|
|
|
const currentLayerConfigs = () =>
|
|
|
|
|
side() === "front"
|
|
|
|
|
? store.state.frontLayerConfigs
|
2026-03-13 17:26:00 +08:00
|
|
|
: store.state.backLayerConfigs;
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
// Available CSV fields not yet used in the current side
|
|
|
|
|
const availableFields = createMemo(() => {
|
|
|
|
|
const cards = store.state.cards;
|
|
|
|
|
if (!cards || cards.length === 0) return [];
|
|
|
|
|
const usedProps = new Set(currentLayerConfigs().map((l) => l.prop));
|
|
|
|
|
return Object.keys(cards[0]).filter(
|
|
|
|
|
(k) => k !== "label" && !usedProps.has(k),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const updateFn = () =>
|
|
|
|
|
side() === "front"
|
|
|
|
|
? store.actions.updateFrontLayerConfig
|
2026-03-13 17:26:00 +08:00
|
|
|
: store.actions.updateBackLayerConfig;
|
2026-06-30 20:55:13 +08:00
|
|
|
|
|
|
|
|
const updateLayerOrientation = (
|
|
|
|
|
index: number,
|
|
|
|
|
orientation: "n" | "s" | "e" | "w",
|
|
|
|
|
) => {
|
|
|
|
|
updateFn()(index, { orientation });
|
2026-03-30 11:40:45 +08:00
|
|
|
setOpenDropdown(null);
|
2026-02-27 22:56:06 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
const updateLayerFontSize = (index: number, fontSize?: number) => {
|
|
|
|
|
updateFn()(index, { fontSize });
|
2026-03-13 17:26:00 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
const updateLayerAlign = (index: number, align?: "l" | "c" | "r") => {
|
|
|
|
|
updateFn()(index, { align });
|
2026-03-30 11:40:45 +08:00
|
|
|
setOpenDropdown(null);
|
2026-03-27 15:17:25 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
const selectLayer = (index: number) => {
|
2026-03-30 12:01:08 +08:00
|
|
|
store.actions.setSelectedLayer(
|
2026-06-30 20:55:13 +08:00
|
|
|
store.state.selectedLayer === index ? null : index,
|
2026-03-13 17:26:00 +08:00
|
|
|
);
|
2026-02-28 12:28:23 +08:00
|
|
|
};
|
|
|
|
|
|
2026-03-30 11:40:45 +08:00
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
2026-06-30 20:55:13 +08:00
|
|
|
if (addMenuRef && !addMenuRef.contains(e.target as Node)) {
|
|
|
|
|
setAddMenuOpen(false);
|
2026-03-30 11:40:45 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
2026-06-30 20:55:13 +08:00
|
|
|
document.addEventListener("click", handleClickOutside);
|
2026-03-30 11:40:45 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onCleanup(() => {
|
2026-06-30 20:55:13 +08:00
|
|
|
document.removeEventListener("click", handleClickOutside);
|
2026-03-30 11:40:45 +08:00
|
|
|
});
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
const onDragEnd: import("@thisbeyond/solid-dnd").DragEventHandler = ({
|
|
|
|
|
draggable,
|
|
|
|
|
droppable,
|
|
|
|
|
}) => {
|
|
|
|
|
if (droppable) {
|
|
|
|
|
const fromIndex = draggable.id as number;
|
|
|
|
|
const toIndex = droppable.id as number;
|
|
|
|
|
if (fromIndex !== toIndex) {
|
|
|
|
|
store.actions.reorderLayers(side(), fromIndex, toIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-30 11:40:45 +08:00
|
|
|
|
2026-02-27 15:33:23 +08:00
|
|
|
return (
|
|
|
|
|
<div class="w-64 flex-shrink-0">
|
2026-03-13 17:26:00 +08:00
|
|
|
<h3 class="font-bold mb-2 mt-0">
|
2026-06-30 20:55:13 +08:00
|
|
|
图层 ({side() === "front" ? "正面" : "背面"})
|
2026-03-13 17:26:00 +08:00
|
|
|
</h3>
|
2026-02-27 15:33:23 +08:00
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
{/* Add layer dropdown */}
|
|
|
|
|
<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()}
|
|
|
|
|
>
|
|
|
|
|
{availableFields().length === 0 ? (
|
|
|
|
|
<div class="px-3 py-2 text-sm text-gray-400">所有字段已添加</div>
|
|
|
|
|
) : (
|
|
|
|
|
<For each={availableFields()}>
|
|
|
|
|
{(field) => (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
store.actions.addLayer(side(), field);
|
|
|
|
|
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"
|
2026-03-30 11:40:45 +08:00
|
|
|
>
|
2026-06-30 20:55:13 +08:00
|
|
|
{field}
|
|
|
|
|
</button>
|
2026-03-30 11:40:45 +08:00
|
|
|
)}
|
2026-06-30 20:55:13 +08:00
|
|
|
</For>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Drag and drop list */}
|
|
|
|
|
<DragDropProvider onDragEnd={onDragEnd} collisionDetector={closestCenter}>
|
|
|
|
|
<DragDropSensors>
|
|
|
|
|
<For each={currentLayerConfigs()}>
|
|
|
|
|
{(layer, index) => (
|
|
|
|
|
<LayerRow
|
|
|
|
|
store={store}
|
|
|
|
|
index={index()}
|
|
|
|
|
layer={layer}
|
|
|
|
|
hovered={hoveredIndex() === index()}
|
|
|
|
|
setHoveredIndex={setHoveredIndex}
|
|
|
|
|
openDropdown={openDropdown()}
|
|
|
|
|
setOpenDropdown={setOpenDropdown}
|
|
|
|
|
updateOrientation={(o) => updateLayerOrientation(index(), o)}
|
|
|
|
|
updateFontSize={(fs) => updateLayerFontSize(index(), fs)}
|
|
|
|
|
updateAlign={(a) => updateLayerAlign(index(), a)}
|
|
|
|
|
onSelect={() => selectLayer(index())}
|
|
|
|
|
onRemove={() => store.actions.removeLayer(side(), index())}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Sortable layer row ---
|
|
|
|
|
|
|
|
|
|
interface LayerRowProps {
|
|
|
|
|
store: DeckStore;
|
|
|
|
|
index: number;
|
|
|
|
|
layer: import("../types").LayerConfig;
|
|
|
|
|
hovered: boolean;
|
|
|
|
|
setHoveredIndex: (idx: number | null) => void;
|
|
|
|
|
openDropdown: string | null;
|
|
|
|
|
setOpenDropdown: (val: string | null) => void;
|
|
|
|
|
updateOrientation: (o: "n" | "s" | "e" | "w") => void;
|
|
|
|
|
updateFontSize: (fs?: number) => void;
|
|
|
|
|
updateAlign: (a?: "l" | "c" | "r") => void;
|
|
|
|
|
onSelect: () => void;
|
|
|
|
|
onRemove: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function LayerRow(props: LayerRowProps) {
|
|
|
|
|
const sortable = createSortable(props.index);
|
|
|
|
|
const [fontOpen, setFontOpen] = createSignal(false);
|
|
|
|
|
let fontDropdownRef: HTMLDivElement | undefined;
|
|
|
|
|
|
|
|
|
|
const selected = () => props.store.state.selectedLayer === props.index;
|
|
|
|
|
|
|
|
|
|
const handleFontClickOutside = (e: MouseEvent) => {
|
|
|
|
|
if (fontDropdownRef && !fontDropdownRef.contains(e.target as Node)) {
|
|
|
|
|
setFontOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
document.addEventListener("click", handleFontClickOutside);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onCleanup(() => {
|
|
|
|
|
document.removeEventListener("click", handleFontClickOutside);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={sortable.ref}
|
|
|
|
|
style={maybeTransformStyle(sortable.transform)}
|
|
|
|
|
class={`flex items-center gap-1 py-1.5 px-1 relative group border-b border-gray-100 ${
|
|
|
|
|
selected() ? "bg-blue-50" : ""
|
|
|
|
|
}`}
|
|
|
|
|
onMouseEnter={() => props.setHoveredIndex(props.index)}
|
|
|
|
|
onMouseLeave={() => props.setHoveredIndex(null)}
|
|
|
|
|
>
|
|
|
|
|
{/* Drag handle */}
|
|
|
|
|
<button
|
|
|
|
|
{...sortable.dragActivators}
|
|
|
|
|
class="cursor-grab text-gray-400 hover:text-gray-600 text-sm px-0.5 select-none shrink-0"
|
|
|
|
|
title="拖动排序"
|
|
|
|
|
>
|
|
|
|
|
⠿
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{/* Visibility toggle */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const toggleFn =
|
|
|
|
|
props.store.state.activeSide === "front"
|
|
|
|
|
? props.store.actions.toggleFrontLayerVisible
|
|
|
|
|
: props.store.actions.toggleBackLayerVisible;
|
|
|
|
|
toggleFn(props.index);
|
|
|
|
|
}}
|
|
|
|
|
class="shrink-0 text-sm cursor-pointer select-none"
|
|
|
|
|
title={props.layer.visible ? "隐藏" : "显示"}
|
|
|
|
|
>
|
|
|
|
|
{props.layer.visible ? "👁" : "👁🗨"}
|
|
|
|
|
</button>
|
2026-03-30 11:40:45 +08:00
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
{/* Layer name */}
|
|
|
|
|
<span
|
|
|
|
|
class="text-sm flex-1 truncate cursor-pointer hover:text-blue-600 select-none"
|
|
|
|
|
onClick={props.onSelect}
|
|
|
|
|
>
|
|
|
|
|
{props.layer.prop}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{/* Orientation */}
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
props.setOpenDropdown(
|
|
|
|
|
props.openDropdown === `orient-${props.index}`
|
|
|
|
|
? null
|
|
|
|
|
: `orient-${props.index}`,
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
class={`w-7 h-7 text-sm rounded cursor-pointer flex items-center justify-center bg-gray-200 text-gray-700 hover:bg-gray-300 ${
|
|
|
|
|
!props.layer.visible ? "invisible pointer-events-none" : ""
|
|
|
|
|
}`}
|
|
|
|
|
title="方向"
|
|
|
|
|
>
|
|
|
|
|
{OrientationIcon(props.layer.orientation || "n")}
|
|
|
|
|
</button>
|
|
|
|
|
{props.openDropdown === `orient-${props.index}` && (
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<For each={ORIENTATION_OPTIONS}>
|
|
|
|
|
{(opt) => (
|
2026-03-30 11:40:45 +08:00
|
|
|
<button
|
2026-06-30 20:55:13 +08:00
|
|
|
onClick={() => props.updateOrientation(opt.value)}
|
|
|
|
|
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
|
2026-03-30 11:40:45 +08:00
|
|
|
>
|
2026-06-30 20:55:13 +08:00
|
|
|
{opt.label}
|
2026-03-30 11:40:45 +08:00
|
|
|
</button>
|
2026-06-30 20:55:13 +08:00
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-03-30 11:40:45 +08:00
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
{/* Align */}
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
props.setOpenDropdown(
|
|
|
|
|
props.openDropdown === `align-${props.index}`
|
|
|
|
|
? null
|
|
|
|
|
: `align-${props.index}`,
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
class={`w-7 h-7 rounded cursor-pointer flex items-center justify-center bg-gray-200 hover:bg-gray-300 ${
|
|
|
|
|
!props.layer.visible ? "invisible pointer-events-none" : ""
|
|
|
|
|
}`}
|
|
|
|
|
title="对齐"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={AlignIconSrc(props.layer.align || "")}
|
|
|
|
|
alt="align"
|
|
|
|
|
class="w-5 h-5 not-prose"
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
{props.openDropdown === `align-${props.index}` && (
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<For each={ALIGN_OPTIONS}>
|
|
|
|
|
{(opt) => (
|
2026-03-30 11:40:45 +08:00
|
|
|
<button
|
2026-06-30 20:55:13 +08:00
|
|
|
onClick={() =>
|
|
|
|
|
props.updateAlign(
|
|
|
|
|
(opt.value as "l" | "c" | "r" | undefined) || undefined,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
class="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
|
2026-03-30 11:40:45 +08:00
|
|
|
>
|
2026-06-30 20:55:13 +08:00
|
|
|
<img
|
|
|
|
|
src={opt.icon}
|
|
|
|
|
alt=""
|
|
|
|
|
class="w-4 h-4 not-prose max-w-none"
|
|
|
|
|
/>
|
2026-03-30 11:40:45 +08:00
|
|
|
</button>
|
2026-06-30 20:55:13 +08:00
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Font size */}
|
|
|
|
|
<div class="relative" ref={fontDropdownRef}>
|
|
|
|
|
<button
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setFontOpen(!fontOpen());
|
|
|
|
|
}}
|
|
|
|
|
class={`w-7 h-7 text-xs rounded cursor-pointer flex items-center justify-center bg-gray-200 text-gray-700 hover:bg-gray-300 ${
|
|
|
|
|
!props.layer.visible ? "invisible pointer-events-none" : ""
|
|
|
|
|
}`}
|
|
|
|
|
title="字体大小 (mm)"
|
|
|
|
|
>
|
|
|
|
|
{props.layer.fontSize ?? 3}
|
|
|
|
|
</button>
|
|
|
|
|
{fontOpen() && (
|
|
|
|
|
<div
|
|
|
|
|
class="absolute top-full right-0 mt-1 bg-white border border-gray-300 rounded shadow-lg z-10 p-2"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center gap-1 mb-2">
|
|
|
|
|
<input
|
|
|
|
|
type="number"
|
|
|
|
|
value={props.layer.fontSize ?? 3}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const value = e.target.value;
|
|
|
|
|
props.updateFontSize(value ? Number(value) : undefined);
|
|
|
|
|
}}
|
|
|
|
|
class="w-14 text-xs px-1.5 py-1 rounded border border-gray-300"
|
|
|
|
|
step="0.1"
|
|
|
|
|
min="0.1"
|
|
|
|
|
/>
|
|
|
|
|
<span class="text-xs text-gray-500">mm</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-1">
|
|
|
|
|
<For each={FONT_PRESETS}>
|
|
|
|
|
{(preset) => (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => props.updateFontSize(preset)}
|
|
|
|
|
class={`px-2 py-1 text-xs rounded cursor-pointer ${
|
|
|
|
|
(props.layer.fontSize ?? 3) === preset
|
|
|
|
|
? "bg-blue-500 text-white"
|
|
|
|
|
: "bg-gray-100 hover:bg-gray-200 text-gray-700"
|
|
|
|
|
}`}
|
2026-03-30 11:40:45 +08:00
|
|
|
>
|
2026-06-30 20:55:13 +08:00
|
|
|
{preset}
|
|
|
|
|
</button>
|
2026-03-30 11:40:45 +08:00
|
|
|
)}
|
2026-06-30 20:55:13 +08:00
|
|
|
</For>
|
2026-02-27 15:33:23 +08:00
|
|
|
</div>
|
2026-06-30 20:55:13 +08:00
|
|
|
<button
|
|
|
|
|
onClick={() => props.updateFontSize(undefined)}
|
|
|
|
|
class="mt-2 w-full text-xs text-gray-500 hover:text-gray-700 cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
重置
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-27 15:33:23 +08:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-30 20:55:13 +08:00
|
|
|
{/* Delete (visible on hover) */}
|
2026-02-27 15:33:23 +08:00
|
|
|
<button
|
2026-06-30 20:55:13 +08:00
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
props.onRemove();
|
|
|
|
|
}}
|
|
|
|
|
class={`shrink-0 w-5 h-5 text-xs rounded cursor-pointer flex items-center justify-center text-red-400 hover:text-red-600 hover:bg-red-50 transition-opacity ${
|
|
|
|
|
props.hovered ? "opacity-100" : "opacity-0"
|
|
|
|
|
}`}
|
|
|
|
|
title="删除图层"
|
2026-02-27 15:33:23 +08:00
|
|
|
>
|
2026-06-30 20:55:13 +08:00
|
|
|
✕
|
2026-02-27 15:33:23 +08:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-03-30 11:40:45 +08:00
|
|
|
|
2026-03-30 12:54:53 +08:00
|
|
|
export { LayerEditorPanel };
|