feat: add Journal server documentation to DocDialog

Introduce a new documentation set for Journal server functionality,
including a dropdown selector in the DocDialog to switch between
"directives" and "journal" documentation modes.
This commit is contained in:
2026-07-07 23:57:31 +08:00
parent 1ef41f04fc
commit c10b088fb1
5 changed files with 265 additions and 6 deletions
+79 -5
View File
@@ -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<DocDialogProps> = (props) => {
const [docSet, setDocSet] = createSignal<DocSet>("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<DocDialogProps> = (props) => {
>
<div class="bg-white rounded-lg shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 shrink-0">
<h2 class="text-lg font-bold text-gray-900"></h2>
<div ref={dropdownRef} class="relative">
<button
onClick={() => setDropdownOpen((v) => !v)}
class="text-lg font-bold text-gray-900 hover:text-blue-600 flex items-center gap-1"
>
{docSetLabel()}
<span class="text-gray-400 text-sm"></span>
</button>
<Show when={dropdownOpen()}>
<div class="absolute top-full left-0 mt-1 w-52 bg-white border border-gray-200 rounded shadow-lg z-50 py-1">
<button
onClick={() => switchDocSet("directives")}
class={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${
docSet() === "directives"
? "bg-blue-50 text-blue-700"
: "text-gray-700"
}`}
>
<span>📖</span>
<span></span>
<Show when={docSet() === "directives"}>
<span class="ml-auto text-blue-500"></span>
</Show>
</button>
<button
onClick={() => switchDocSet("journal")}
class={`w-full text-left px-3 py-2 text-sm hover:bg-gray-100 flex items-center gap-2 ${
docSet() === "journal"
? "bg-blue-50 text-blue-700"
: "text-gray-700"
}`}
>
<span>📋</span>
<span>Journal </span>
<Show when={docSet() === "journal"}>
<span class="ml-auto text-blue-500"></span>
</Show>
</button>
</div>
</Show>
</div>
<button
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-xl leading-none p-1"
@@ -53,7 +122,7 @@ const DocDialog: Component<DocDialogProps> = (props) => {
<div class="flex flex-1 min-h-0">
<nav class="w-48 shrink-0 border-r border-gray-200 overflow-y-auto p-3 bg-gray-50">
<For each={docEntries}>
<For each={currentEntries()}>
{(entry) => (
<button
onClick={() => setSelectedTag(entry.tag)}
@@ -64,7 +133,12 @@ const DocDialog: Component<DocDialogProps> = (props) => {
}`}
>
<span class="mr-2">{entry.icon}</span>
<span class="font-mono text-xs">{`:${entry.tag}`}</span>
<Show
when={docSet() === "directives"}
fallback={<span class="text-xs">{entry.title}</span>}
>
<span class="font-mono text-xs">{`:${entry.tag}`}</span>
</Show>
</button>
)}
</For>
@@ -83,7 +157,7 @@ const DocDialog: Component<DocDialogProps> = (props) => {
};
/** Single entry documentation content */
const DocContent: Component<{ entry: DocEntry }> = (props) => {
const DocContent: Component<{ entry: AnyEntry }> = (props) => {
const e = props.entry;
return (
+57
View File
@@ -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<string, unknown> | 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<string, unknown>;
} 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();