ttrpg-tools/src/components/md-yarn-spinner.tsx

112 lines
3.4 KiB
TypeScript
Raw Normal View History

2026-03-02 16:49:55 +08:00
import { customElement, noShadowDOM } from 'solid-element';
2026-03-03 10:17:39 +08:00
import { For, Show, createEffect } from 'solid-js';
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
import { createYarnStore } from './stores/yarnStore';
2026-03-02 16:49:55 +08:00
2026-03-03 14:57:46 +08:00
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
2026-03-02 16:49:55 +08:00
noShadowDOM();
2026-03-03 10:17:39 +08:00
let historyContainer: HTMLDivElement | undefined;
const { store, advance, restart } = createYarnStore(element as any, props);
// 滚动到底部
const scrollToBottom = () => {
if (historyContainer) {
setTimeout(() => {
historyContainer!.scrollTop = historyContainer!.scrollHeight;
}, 0);
2026-03-02 16:49:55 +08:00
}
};
2026-03-03 10:17:39 +08:00
// 监听对话历史变化,自动滚动到底部
2026-03-02 16:49:55 +08:00
createEffect(() => {
2026-03-03 10:17:39 +08:00
store.dialogueHistory;
scrollToBottom();
2026-03-02 16:49:55 +08:00
});
2026-03-03 10:17:39 +08:00
2026-03-02 16:49:55 +08:00
// 渲染对话历史项
2026-03-03 10:17:39 +08:00
const renderEntry = (result: RuntimeResult | OptionsResult['options'][0]) => {
if(!('type' in result)){
return <div class="dialogue-entry text-entry">
<span class="text"> {"->"} {result.text}</span>
</div>
}
2026-03-02 16:49:55 +08:00
if (result.type === 'text') {
const textResult = result as TextResult;
return (
<div class="dialogue-entry text-entry">
<Show when={textResult.speaker}>
<span class="speaker font-bold text-blue-600">{textResult.speaker}: </span>
</Show>
<span class="text">{textResult.text}</span>
</div>
);
}
2026-03-03 10:17:39 +08:00
2026-03-02 16:49:55 +08:00
if (result.type === 'command') {
return (
<div class="dialogue-entry command-entry text-gray-500 italic">
[{result.command}]
</div>
);
}
2026-03-03 10:17:39 +08:00
2026-03-02 16:49:55 +08:00
return null;
};
2026-03-03 10:17:39 +08:00
2026-03-02 16:49:55 +08:00
return (
2026-03-03 10:17:39 +08:00
<div class="w-full max-w-2xl mx-auto shadow-sm relative">
2026-03-02 16:49:55 +08:00
{/* 对话历史 */}
2026-03-03 10:17:39 +08:00
<div
ref={historyContainer}
class="dialogue-history p-4 h-64 overflow-y-auto bg-gray-50"
>
<Show when={store.dialogueHistory.length === 0 && !store.runnerInstance}>
2026-03-02 16:49:55 +08:00
<div class="text-gray-400 text-center py-8"></div>
</Show>
2026-03-03 10:17:39 +08:00
<For each={store.dialogueHistory}>
2026-03-02 17:54:40 +08:00
{renderEntry}
2026-03-02 16:49:55 +08:00
</For>
</div>
2026-03-03 00:48:13 +08:00
2026-03-03 10:17:39 +08:00
{/* 浮动工具栏 */}
<div class="toolbar absolute right-0 rounded-bl-lg shadow-sm flex">
<button
onClick={restart}
class="restart-button px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300
rounded-bl-md transition-colors cursor-pointer"
title="重新开始"
>
🔄
</button>
<button
onClick={() => advance()}
class="advance-button px-3 py-1 text-sm bg-gray-200 hover:bg-gray-300
transition-colors cursor-pointer"
title="继续"
>
</button>
</div>
2026-03-02 16:49:55 +08:00
{/* 当前选项 */}
2026-03-03 10:17:39 +08:00
<div class="current-options p-4 border-t border-gray-300 bg-white min-h-34">
2026-03-02 16:49:55 +08:00
<div class="options-list flex flex-col gap-2">
2026-03-03 10:17:39 +08:00
<For each={store.currentOptions?.options || []}>
2026-03-02 16:49:55 +08:00
{(option, index) => (
<button
2026-03-02 17:54:40 +08:00
onClick={() => advance(index())}
2026-03-03 00:48:13 +08:00
class="option-button text-left px-4 py-2 bg-blue-50 hover:bg-blue-100
2026-03-02 16:49:55 +08:00
rounded border border-blue-200 transition-colors cursor-pointer"
>
{option.text}
</button>
)}
</For>
</div>
</div>
</div>
);
});