diff --git a/src/components/DocDialog.tsx b/src/components/DocDialog.tsx index d566106..321f31f 100644 --- a/src/components/DocDialog.tsx +++ b/src/components/DocDialog.tsx @@ -7,6 +7,10 @@ import { onMount, } from "solid-js"; import { docEntries, type DocEntry } from "./doc-data"; +import { journalDocEntries, type JournalDocEntry } from "./journal-doc-data"; + +type DocSet = "directives" | "journal"; +type AnyEntry = DocEntry | JournalDocEntry; export interface DocDialogProps { isOpen: boolean; @@ -14,17 +18,42 @@ export interface DocDialogProps { } const DocDialog: Component = (props) => { + const [docSet, setDocSet] = createSignal("directives"); const [selectedTag, setSelectedTag] = createSignal(docEntries[0]?.tag ?? ""); + const [dropdownOpen, setDropdownOpen] = createSignal(false); + let dropdownRef!: HTMLDivElement; + + const currentEntries = () => + docSet() === "directives" + ? (docEntries as AnyEntry[]) + : (journalDocEntries as AnyEntry[]); const selectedEntry = () => - docEntries.find((e) => e.tag === selectedTag()) ?? docEntries[0]; + currentEntries().find((e) => e.tag === selectedTag()) ?? + currentEntries()[0]; + + const docSetLabel = () => + docSet() === "directives" ? "指令组件文档" : "Journal 服务器文档"; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") props.onClose(); }; + const switchDocSet = (set: DocSet) => { + setDocSet(set); + setDropdownOpen(false); + const entries = set === "directives" ? docEntries : journalDocEntries; + setSelectedTag(entries[0]?.tag ?? ""); + }; + onMount(() => { document.addEventListener("keydown", handleKeyDown); + // Close dropdown on outside click + document.addEventListener("click", (e) => { + if (dropdownRef && !dropdownRef.contains(e.target as Node)) { + setDropdownOpen(false); + } + }); }); onCleanup(() => { @@ -41,7 +70,47 @@ const DocDialog: Component = (props) => { >
-

指令组件文档

+
+ + +
+ + +
+
+
)} @@ -83,7 +157,7 @@ const DocDialog: Component = (props) => { }; /** Single entry documentation content */ -const DocContent: Component<{ entry: DocEntry }> = (props) => { +const DocContent: Component<{ entry: AnyEntry }> = (props) => { const e = props.entry; return ( diff --git a/src/components/journal-doc-data.ts b/src/components/journal-doc-data.ts new file mode 100644 index 0000000..af3751d --- /dev/null +++ b/src/components/journal-doc-data.ts @@ -0,0 +1,57 @@ +import yaml from "js-yaml"; + +export interface JournalDocEntry { + tag: string; + icon: string; + title: string; + description: string; + syntax: string; + props: { name: string; type: string; default?: string; desc: string }[]; + body: string; +} + +function parseFrontmatter(raw: string): Record | null { + const normalized = raw.replace(/\r\n/g, "\n"); + const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); + if (!match) return null; + try { + return yaml.load(match[1]) as Record; + } catch { + return null; + } +} + +function getBody(raw: string): string { + const normalized = raw.replace(/\r\n/g, "\n"); + const match = normalized.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/); + return match ? match[1] : raw; +} + +function parseEntry(raw: string): JournalDocEntry | null { + const fm = parseFrontmatter(raw); + if (!fm) return null; + return { + tag: fm.tag as string, + icon: fm.icon as string, + title: fm.title as string, + description: fm.description as string, + syntax: fm.syntax as string, + props: (fm.props as JournalDocEntry["props"]) ?? [], + body: getBody(raw), + }; +} + +import journalGmRaw from "../doc-entries/journal-gm.md"; +import journalPlayerRaw from "../doc-entries/journal-player.md"; + +const rawDocuments: string[] = [journalGmRaw, journalPlayerRaw]; + +let _entries: JournalDocEntry[] | null = null; + +function loadJournalDocEntries(): JournalDocEntry[] { + if (_entries) return _entries; + _entries = rawDocuments.map(parseEntry).filter(Boolean) as JournalDocEntry[]; + return _entries; +} + +export const journalDocEntries = loadJournalDocEntries(); diff --git a/src/doc-entries/journal-gm.md b/src/doc-entries/journal-gm.md new file mode 100644 index 0000000..17a10bd --- /dev/null +++ b/src/doc-entries/journal-gm.md @@ -0,0 +1,62 @@ +--- +tag: journal-gm +icon: 🎭 +title: 主持人指南 +description: 作为 GM 创建会话、管理玩家、使用动态表单和消息类型。 +syntax: '点击导航栏 📋 图标打开 Journal 面板' +props: + - name: 会话管理 + type: — + desc: 创建、切换、删除会话;通过邀请链接添加玩家 + - name: 消息类型 + type: — + desc: 发送叙述、掷骰、表单等多种消息类型 + - name: 动态表单 + type: — + desc: 创建自定义表单收集玩家输入 +--- + +## 快速开始 + +1. 点击顶部导航栏的 📋 按钮打开 Journal 面板 +2. 输入你的名字,选择"主持人"角色,点击"加入" +3. 连接成功后,你可以创建新会话或选择已有会话 + +## 会话管理 + +作为 GM,你可以管理多个会话: + +- **创建会话**:点击标题下方的会话名称 → 下拉菜单 → "+ 新建会话" +- **切换会话**:点击会话名称,从下拉列表中选择 +- **邀请玩家**:点击玩家标签旁的 "+ 邀请",输入玩家名字,复制链接发送给玩家 + +玩家通过邀请链接加入后会自动以玩家身份连接。 + +## 消息类型 + +Journal 支持多种消息类型,通过输入框上方的标签切换: + +- **叙述**:普通的文字描述,用于推进剧情 +- **掷骰**:发送掷骰结果,可附带公式 +- **表单**:创建动态表单,收集玩家选择或输入 + +## 动态表单 + +表单消息允许你创建交互式问卷: + +- 添加文本输入、选择框、复选框等字段 +- 玩家提交后,GM 可以看到汇总结果 +- 适用于投票、决策、角色创建等场景 + +## 撤回消息 + +GM 可以撤回自己发送的最新消息,点击消息旁的撤回按钮即可。 + +## 连接状态 + +- 🟢 绿色:已连接 +- 🟡 黄色闪烁:连接中 +- 🔴 红色:连接错误 +- ⚪ 灰色:未连接 + +断开连接可点击标题栏的 ⏻ 按钮。 diff --git a/src/doc-entries/journal-player.md b/src/doc-entries/journal-player.md new file mode 100644 index 0000000..5d6b050 --- /dev/null +++ b/src/doc-entries/journal-player.md @@ -0,0 +1,66 @@ +--- +tag: journal-player +icon: 🎲 +title: 玩家指南 +description: 作为玩家加入 GM 的会话,查看消息、提交表单、参与互动。 +syntax: '通过 GM 发送的邀请链接加入' +props: + - name: 加入方式 + type: — + desc: 点击邀请链接自动加入,或手动输入名字和角色 + - name: 查看消息 + type: — + desc: 实时查看 GM 和其他玩家的消息 + - name: 提交表单 + type: — + desc: 填写 GM 发送的动态表单并提交 +--- + +## 加入会话 + +有两种方式加入 GM 的会话: + +### 通过邀请链接(推荐) + +GM 会发送一个包含 `?session=xxx&player=你的名字&autojoin=1` 的链接。 +点击链接即可自动加入,无需手动输入任何信息。 + +### 手动加入 + +1. 点击顶部导航栏的 📋 按钮打开 Journal 面板 +2. 输入你的名字 +3. 选择"玩家"角色 +4. 点击"加入" + +## 查看消息 + +连接成功后,你可以在面板中看到: + +- GM 发送的叙述消息 +- 掷骰结果 +- 其他玩家的消息 +- 动态表单 + +消息实时更新,无需刷新页面。 + +## 填写表单 + +当 GM 发送动态表单时: + +1. 表单会显示在消息流中 +2. 根据表单类型填写(文本、选择、复选框等) +3. 点击提交按钮发送你的回答 + +GM 可以看到所有玩家的提交结果。 + +## 角色标识 + +- 你的名字旁会显示蓝色"player"标签 +- 观察者显示灰色"observer"标签 +- GM 不显示角色标签 + +## 注意事项 + +- 玩家无法创建或切换会话 +- 玩家无法撤回消息 +- 断开连接后重新加入会保留之前的消息记录 diff --git a/src/markdown/table.ts b/src/markdown/table.ts index fc36ae2..10e7580 100644 --- a/src/markdown/table.ts +++ b/src/markdown/table.ts @@ -28,7 +28,7 @@ function tableToCSV(headers: string[], rows: string[][]): string { } /** Spark table: first column header is a pure dice formula (d6, d20, etc.) */ -const SPARK_DICE_RE = /^d\d+$/i; +const SPARK_DICE_RE = /^\d*d\d+$/i; export default function markedTable(): MarkedExtension { return {