Files
ttrpg-tools/src/components/md-pins.tsx
T
hypercross 40b6fbe157 feat(ui): replace unicode icons with solid-icons
Swap emoji and unicode glyphs for Tabler components so icons inherit
currentColor and font-size and render consistently across platforms.

- add src/components/doc-icons.ts to resolve doc-entry frontmatter icon
  keys to components; frontmatter now stores semantic keys
- replace the align-*.png assets with TbOutlineAlign* icons
- render RevealManager action SVGs as components instead of injected
  innerHTML strings
- carry completion icons via an optional CompletionItem.icon field
2026-09-13 14:01:03 +08:00

148 lines
4.6 KiB
TypeScript

import { customElement, noShadowDOM } from "solid-element";
import {Show, For, createResource, createMemo, createSignal} from "solid-js";
import { TbOutlineCopy } from "solid-icons/tb";
import { resolvePath } from "./utils/path";
import { createPinsStore } from "./stores/pinsStore";
customElement("md-pins", { pins: "", fixed: false, labelStart: "A" }, (props, { element }) => {
noShadowDOM();
const [showToast, setShowToast] = createSignal(false);
// 创建 store
const store = createPinsStore(props.pins, props.fixed, props.labelStart);
// 从 element 的 textContent 获取图片路径
const rawSrc = element?.textContent?.trim() || '';
// 设置 rawSrc 到 store
store.setRawSrc(rawSrc);
// 隐藏原始文本内容
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, rawSrc);
// 加载图片
const loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
};
const [image] = createResource(resolvedSrc, loadImage);
const visible = createMemo(() => !image.loading && !!image());
// 添加 pin
const addPin = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (props.fixed) return;
const imgRect = (e.target as Element).getBoundingClientRect();
const clickX = ((e.clientX - imgRect.left) / imgRect.width) * 100;
const clickY = ((e.clientY - imgRect.top) / imgRect.height) * 100;
const x = Math.round(clickX);
const y = Math.round(clickY);
store.addPin(x, y);
};
// 删除 pin
const removePin = (index: number, e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (props.fixed) return;
store.removePin(index);
};
// 复制所有 pin 为 :md-editor-pin 格式
const copyPins = () => {
const text = store.getCopyText();
navigator.clipboard.writeText(text).then(() => {
setShowToast(true);
setTimeout(() => setShowToast(false), 2000);
}).catch(err => {
console.error('复制失败:', err);
});
};
return (
<div>
<Show when={visible()}>
{/* 图片容器 */}
<div class="relative" onClick={addPin}>
{/* 显示图片 */}
<img src={resolvedSrc} alt="" class="inset-0" />
{/* 透明遮罩层 */}
<Show when={!props.fixed}>
<div class="absolute inset-0 bg-transparent hover:bg-black/10 transition-colors cursor-crosshair" />
</Show>
<Show when={props.fixed}>
<div class="absolute inset-0 pointer-events-none" />
</Show>
{/* 复制按钮 HUD */}
<Show when={!props.fixed}>
<div class="absolute top-2 right-2 z-20">
<button
onClick={(e) => {
e.stopPropagation();
copyPins();
}}
class="bg-gray-800 hover:bg-gray-700 text-white px-3 py-1.5 rounded shadow-lg text-sm flex items-center gap-1 transition-colors"
title="复制所有 pin 坐标"
>
<TbOutlineCopy /> 复制
</button>
</div>
</Show>
{/* Pin 列表 */}
<For each={store.state.pins}>
{(pin, index) => (
<span
onClick={(e) => removePin(index(), e)}
class={`absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto
bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6
flex items-center justify-center shadow-lg
${!props.fixed ? 'cursor-pointer hover:bg-red-600 hover:scale-110 transition-all z-10' : 'cursor-default z-10'}`}
style={{
left: `${pin.x}%`,
top: `${pin.y}%`
}}
title={props.fixed ? `(${pin.x}, ${pin.y})` : `点击删除 (${pin.x}, ${pin.y})`}
>
{pin.label}
</span>
)}
</For>
</div>
</Show>
{/* Toast 提示 */}
<Show when={showToast()}>
<div class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded shadow-lg text-sm z-50">
已复制 {store.state.pins.length} 个 pin 坐标
</div>
</Show>
</div>
);
});