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
This commit is contained in:
2026-09-13 14:01:03 +08:00
parent b5f25e6b60
commit 40b6fbe157
50 changed files with 304 additions and 152 deletions
+8 -3
View File
@@ -7,6 +7,11 @@ import {
createSignal, createSignal,
} from "solid-js"; } from "solid-js";
import { useLocation } from "@solidjs/router"; import { useLocation } from "@solidjs/router";
import {
TbOutlineMenu2,
TbOutlineClipboardList,
TbOutlineHelpCircle,
} from "solid-icons/tb";
import { useJournalStream } from "./components/stores/journalStream"; import { useJournalStream } from "./components/stores/journalStream";
// 导入组件以注册自定义元素 // 导入组件以注册自定义元素
@@ -99,7 +104,7 @@ const App: Component = () => {
class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100" class="md:hidden text-gray-600 hover:text-gray-900 p-1 rounded hover:bg-gray-100"
title="目录" title="目录"
> >
☰ <TbOutlineMenu2 class="inline-block align-[-0.125em]" />
</button> </button>
<h1 class="text-2xl font-bold text-gray-900">Tabletop Toolkit</h1> <h1 class="text-2xl font-bold text-gray-900">Tabletop Toolkit</h1>
<div class="flex-1" /> <div class="flex-1" />
@@ -122,14 +127,14 @@ const App: Component = () => {
"bg-red-500": stream.connectionStatus === "error", "bg-red-500": stream.connectionStatus === "error",
}} }}
/> />
📋 <TbOutlineClipboardList size={16} />
</button> </button>
<button <button
onClick={() => setIsDocOpen(true)} onClick={() => setIsDocOpen(true)}
class="w-8 h-8 rounded-full border border-gray-300 text-gray-500 hover:text-gray-700 hover:border-gray-400 flex items-center justify-center text-sm font-medium" class="w-8 h-8 rounded-full border border-gray-300 text-gray-500 hover:text-gray-700 hover:border-gray-400 flex items-center justify-center text-sm font-medium"
title="指令文档" title="指令文档"
> >
? <TbOutlineHelpCircle size={16} />
</button> </button>
</div> </div>
</header> </header>
+24 -8
View File
@@ -6,8 +6,17 @@ import {
onCleanup, onCleanup,
onMount, onMount,
} from "solid-js"; } from "solid-js";
import { Dynamic } from "solid-js/web";
import {
TbOutlineBook2,
TbOutlineCheck,
TbOutlineChevronDown,
TbOutlineClipboardList,
TbOutlineX,
} from "solid-icons/tb";
import { docEntries, type DocEntry } from "./doc-data"; import { docEntries, type DocEntry } from "./doc-data";
import { journalDocEntries, type JournalDocEntry } from "./journal-doc-data"; import { journalDocEntries, type JournalDocEntry } from "./journal-doc-data";
import { docIcon } from "./doc-icons";
import { parseMarkdown } from "../markdown"; import { parseMarkdown } from "../markdown";
type DocSet = "directives" | "journal"; type DocSet = "directives" | "journal";
@@ -77,7 +86,7 @@ const DocDialog: Component<DocDialogProps> = (props) => {
class="text-lg font-bold text-gray-900 hover:text-blue-600 flex items-center gap-1" class="text-lg font-bold text-gray-900 hover:text-blue-600 flex items-center gap-1"
> >
{docSetLabel()} {docSetLabel()}
<span class="text-gray-400 text-sm">▾</span> <TbOutlineChevronDown class="text-gray-400 text-sm" />
</button> </button>
<Show when={dropdownOpen()}> <Show when={dropdownOpen()}>
<div class="absolute top-full left-0 mt-1 w-52 bg-white border border-gray-200 rounded shadow-lg z-60 py-1"> <div class="absolute top-full left-0 mt-1 w-52 bg-white border border-gray-200 rounded shadow-lg z-60 py-1">
@@ -89,10 +98,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "text-gray-700" : "text-gray-700"
}`} }`}
> >
<span>📖</span> <TbOutlineBook2 />
<span>指令组件文档</span> <span>指令组件文档</span>
<Show when={docSet() === "directives"}> <Show when={docSet() === "directives"}>
<span class="ml-auto text-blue-500">✓</span> <TbOutlineCheck class="ml-auto text-blue-500" />
</Show> </Show>
</button> </button>
<button <button
@@ -103,10 +112,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "text-gray-700" : "text-gray-700"
}`} }`}
> >
<span>📋</span> <TbOutlineClipboardList />
<span>Journal 服务器文档</span> <span>Journal 服务器文档</span>
<Show when={docSet() === "journal"}> <Show when={docSet() === "journal"}>
<span class="ml-auto text-blue-500">✓</span> <TbOutlineCheck class="ml-auto text-blue-500" />
</Show> </Show>
</button> </button>
</div> </div>
@@ -117,7 +126,7 @@ const DocDialog: Component<DocDialogProps> = (props) => {
class="text-gray-400 hover:text-gray-600 text-xl leading-none p-1" class="text-gray-400 hover:text-gray-600 text-xl leading-none p-1"
title="关闭" title="关闭"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
@@ -133,7 +142,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "text-gray-700 hover:bg-gray-100" : "text-gray-700 hover:bg-gray-100"
}`} }`}
> >
<span class="mr-2">{entry.icon}</span> <Dynamic
component={docIcon(entry.icon)}
class="mr-2 inline-block align-[-0.125em]"
/>
<Show <Show
when={docSet() === "directives"} when={docSet() === "directives"}
fallback={<span class="text-xs">{entry.title}</span>} fallback={<span class="text-xs">{entry.title}</span>}
@@ -164,7 +176,11 @@ const DocContent: Component<{ entry: AnyEntry }> = (props) => {
return ( return (
<div class="max-w-none"> <div class="max-w-none">
<h2 class="text-2xl font-bold text-gray-900 mb-6"> <h2 class="text-2xl font-bold text-gray-900 mb-6">
{e.icon} {e.title} <Dynamic
component={docIcon(e.icon)}
class="inline-block align-[-0.125em]"
/>{" "}
{e.title}
</h2> </h2>
<article <article
+13 -3
View File
@@ -1,5 +1,11 @@
import { Component, createMemo, createSignal, Show } from "solid-js"; import { Component, createMemo, createSignal, Show } from "solid-js";
import { useLocation } from "@solidjs/router"; import { useLocation } from "@solidjs/router";
import {
TbOutlineFolderOpen,
TbOutlineFolder,
TbOutlineFile,
TbOutlineChevronRight,
} from "solid-icons/tb";
import { type FileNode, type TocNode } from "../data-loader"; import { type FileNode, type TocNode } from "../data-loader";
import { useNavigateWithParams } from "./useNavigateWithParams"; import { useNavigateWithParams } from "./useNavigateWithParams";
import { useScrollContainer } from "../App"; import { useScrollContainer } from "../App";
@@ -61,10 +67,14 @@ export const FileTreeNode: Component<{
onClick={handleClick} onClick={handleClick}
> >
<Show when={isDir}> <Show when={isDir}>
<span class="mr-1 text-gray-400">{isExpanded() ? "📂" : "📁"}</span> {isExpanded() ? (
<TbOutlineFolderOpen class="mr-1 text-gray-400" />
) : (
<TbOutlineFolder class="mr-1 text-gray-400" />
)}
</Show> </Show>
<Show when={!isDir}> <Show when={!isDir}>
<span class="mr-1 text-gray-400">📄</span> <TbOutlineFile class="mr-1 text-gray-400" />
</Show> </Show>
<span class="text-sm truncate">{props.node.name}</span> <span class="text-sm truncate">{props.node.name}</span>
</a> </a>
@@ -151,7 +161,7 @@ export const HeadingNode: Component<{
<span <span
class={`${hasChildren ? "" : "invisible"} ${isExpanded() ? "rotate-90" : ""} mt-1 inline-block transition-transform `} class={`${hasChildren ? "" : "invisible"} ${isExpanded() ? "rotate-90" : ""} mt-1 inline-block transition-transform `}
> >
▶ <TbOutlineChevronRight class="inline-block align-[-0.125em]" />
</span> </span>
</span> </span>
<a <a
+9 -25
View File
@@ -15,8 +15,10 @@ import {
onCleanup, onCleanup,
Show, Show,
} from "solid-js"; } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal, Dynamic } from "solid-js/web";
import { useLocation } from "@solidjs/router"; import { useLocation } from "@solidjs/router";
import type { IconTypes } from "solid-icons";
import { TbOutlineLink, TbOutlineSparkles } from "solid-icons/tb";
import { useArticleDom } from "./Article"; import { useArticleDom } from "./Article";
import { journalStreamState } from "./stores/journalStream"; import { journalStreamState } from "./stores/journalStream";
import { useJournalCompletions } from "./journal/completions"; import { useJournalCompletions } from "./journal/completions";
@@ -27,25 +29,6 @@ import {
normalizePath, normalizePath,
} from "./stores/reveal"; } from "./stores/reveal";
// ---------------------------------------------------------------------------
// SVG icons
// ---------------------------------------------------------------------------
const LINK_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" ' +
'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
'stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>' +
'<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>' +
"</svg>";
const SPARK_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" ' +
'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
'stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M12 2l1.5 6.5L18 7l-4.5 4.5L16 16l-4-2.5L8 16l1.5-4.5L5 7l4.5-.5z"/>' +
"</svg>";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Floating button state // Floating button state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -54,7 +37,7 @@ interface FloatingButton {
rect: DOMRect; rect: DOMRect;
action: () => void; action: () => void;
title: string; title: string;
svg: string; icon: IconTypes;
color: string; color: string;
} }
@@ -130,7 +113,7 @@ export const RevealManager: Component = () => {
text: combinedSlug, text: combinedSlug,
}), }),
title: "Roll spark table", title: "Roll spark table",
svg: SPARK_SVG, icon: TbOutlineSparkles,
color: "text-purple-400 hover:text-purple-600 hover:bg-purple-50", color: "text-purple-400 hover:text-purple-600 hover:bg-purple-50",
}); });
return; return;
@@ -150,7 +133,7 @@ export const RevealManager: Component = () => {
text: `${normalized}#${text}`, text: `${normalized}#${text}`,
}), }),
title: "Send /link to stream", title: "Send /link to stream",
svg: LINK_SVG, icon: TbOutlineLink,
color: "text-blue-500 hover:bg-blue-50", color: "text-blue-500 hover:bg-blue-50",
}); });
} }
@@ -183,14 +166,15 @@ export const RevealManager: Component = () => {
left: `${btn().rect.left - 20}px`, left: `${btn().rect.left - 20}px`,
}} }}
title={btn().title} title={btn().title}
innerHTML={btn().svg}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
btn().action(); btn().action();
}} }}
onMouseEnter={() => clearTimeout(hideTimer)} onMouseEnter={() => clearTimeout(hideTimer)}
onMouseLeave={hide} onMouseLeave={hide}
/> >
<Dynamic component={btn().icon} size={12} />
</button>
</Portal> </Portal>
)} )}
</Show> </Show>
+3 -2
View File
@@ -1,6 +1,7 @@
import { Component, createMemo, createSignal, onMount, Show } from "solid-js"; import { Component, createMemo, createSignal, onMount, Show } from "solid-js";
import { generateToc, type FileNode, type TocNode } from "../data-loader"; import { generateToc, type FileNode, type TocNode } from "../data-loader";
import { useLocation } from "@solidjs/router"; import { useLocation } from "@solidjs/router";
import { TbOutlineFolderOpen, TbOutlineX } from "solid-icons/tb";
import { FileTreeNode, HeadingNode } from "./FileTree"; import { FileTreeNode, HeadingNode } from "./FileTree";
import { isPathRevealed } from "./stores/reveal"; import { isPathRevealed } from "./stores/reveal";
@@ -57,7 +58,7 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
class="text-gray-400 hover:text-gray-600 p-1 rounded hover:bg-gray-100" class="text-gray-400 hover:text-gray-600 p-1 rounded hover:bg-gray-100"
title="Content Source" title="Content Source"
> >
📂 <TbOutlineFolderOpen class="inline-block align-[-0.125em]" />
</button> </button>
<Show when={!props.isDesktop}> <Show when={!props.isDesktop}>
<button <button
@@ -65,7 +66,7 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
class="text-gray-500 hover:text-gray-700" class="text-gray-500 hover:text-gray-700"
title="关闭" title="关闭"
> >
✕ <TbOutlineX class="inline-block align-[-0.125em]" />
</button> </button>
</Show> </Show>
</div> </div>
+48
View File
@@ -0,0 +1,48 @@
/**
* Icon registry for built-in doc entries.
*
* Doc-entry frontmatter `icon:` values are semantic keys (e.g. `dice`), not
* glyphs — YAML can't reference a component. This maps each key to a Tabler
* icon component; unknown keys fall back to a generic file icon.
*/
import type { IconTypes } from "solid-icons";
import {
TbOutlineCards,
TbOutlineDice5,
TbOutlineFile,
TbOutlineFileText,
TbOutlineGitBranch,
TbOutlineHash,
TbOutlineLink,
TbOutlineMapPin,
TbOutlinePhoto,
TbOutlineSparkles,
TbOutlineSquare,
TbOutlineTable,
TbOutlineTheater,
TbOutlineTypography,
} from "solid-icons/tb";
const DOC_ICONS: Record<string, IconTypes> = {
dice: TbOutlineDice5,
table: TbOutlineTable,
link: TbOutlineLink,
pins: TbOutlineMapPin,
font: TbOutlineTypography,
background: TbOutlinePhoto,
border: TbOutlineSquare,
embed: TbOutlineFileText,
deck: TbOutlineCards,
yarn: TbOutlineGitBranch,
gm: TbOutlineTheater,
player: TbOutlineDice5,
roll: TbOutlineDice5,
set: TbOutlineHash,
spark: TbOutlineSparkles,
};
/** Resolve a frontmatter icon key to a Tabler icon component. */
export function docIcon(key: string): IconTypes {
return DOC_ICONS[key] ?? TbOutlineFile;
}
@@ -3,6 +3,7 @@
*/ */
import { Component, For, Show, createEffect, onMount, onCleanup } from "solid-js"; import { Component, For, Show, createEffect, onMount, onCleanup } from "solid-js";
import { Dynamic } from "solid-js/web";
import type { CompletionItem } from "./command-parser"; import type { CompletionItem } from "./command-parser";
import type { CompletionsState } from "./completions"; import type { CompletionsState } from "./completions";
@@ -102,6 +103,11 @@ export const CompletionsDropdown: Component<CompletionsDropdownProps> = (
{item.kind === "command" ? "cmd" : "val"} {item.kind === "command" ? "cmd" : "val"}
</span> </span>
</Show> </Show>
<Show when={item.icon}>
{(icon) => (
<Dynamic component={icon()} size={14} class="shrink-0" />
)}
</Show>
<span class="font-mono truncate flex-1">{item.label}</span> <span class="font-mono truncate flex-1">{item.label}</span>
</div> </div>
)} )}
+2 -1
View File
@@ -6,6 +6,7 @@
* or creating a session is a separate step that reuses that connection. * or creating a session is a separate step that reuses that connection.
*/ */
import { Component, createSignal, For, onMount, Show } from "solid-js"; import { Component, createSignal, For, onMount, Show } from "solid-js";
import { TbOutlineTrash } from "solid-icons/tb";
import { import {
connectBroker, connectBroker,
createSession, createSession,
@@ -167,7 +168,7 @@ export const ConnectDialog: Component = () => {
class="shrink-0 px-2 py-1.5 text-[10px] text-gray-300 hover:text-red-500 disabled:opacity-50" class="shrink-0 px-2 py-1.5 text-[10px] text-gray-300 hover:text-red-500 disabled:opacity-50"
title="删除会话" title="删除会话"
> >
🗑 <TbOutlineTrash />
</button> </button>
</Show> </Show>
</div> </div>
@@ -2,6 +2,7 @@
* CreateSessionDialog — modal for naming a new session * CreateSessionDialog — modal for naming a new session
*/ */
import { Component, createSignal, onMount } from "solid-js"; import { Component, createSignal, onMount } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { createSession, joinSession } from "../stores/journalStream"; import { createSession, joinSession } from "../stores/journalStream";
interface CreateSessionDialogProps { interface CreateSessionDialogProps {
@@ -48,7 +49,7 @@ export const CreateSessionDialog: Component<CreateSessionDialogProps> = (
onClick={props.onClose} onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm" class="text-gray-400 hover:text-gray-600 text-sm"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
<p class="text-xs text-gray-500">输入新会话的名称。</p> <p class="text-xs text-gray-500">输入新会话的名称。</p>
@@ -5,6 +5,7 @@
* log), so the warning spells out that it cannot be undone. * log), so the warning spells out that it cannot be undone.
*/ */
import { Component, createSignal, Show } from "solid-js"; import { Component, createSignal, Show } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { deleteSession } from "../stores/journalStream"; import { deleteSession } from "../stores/journalStream";
interface DeleteSessionDialogProps { interface DeleteSessionDialogProps {
@@ -49,7 +50,7 @@ export const DeleteSessionDialog: Component<DeleteSessionDialogProps> = (
onClick={props.onClose} onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm" class="text-gray-400 hover:text-gray-600 text-sm"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
<p class="text-xs text-gray-600"> <p class="text-xs text-gray-600">
+2 -1
View File
@@ -3,6 +3,7 @@
* the journal input area. Toggled by clicking the truncated error text. * the journal input area. Toggled by clicking the truncated error text.
*/ */
import { Component, Show } from "solid-js"; import { Component, Show } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
interface ErrorPopupProps { interface ErrorPopupProps {
message: string | null; message: string | null;
@@ -21,7 +22,7 @@ export const ErrorPopup: Component<ErrorPopupProps> = (props) => {
class="text-red-400 hover:text-red-600 text-xs leading-none ml-2 shrink-0" class="text-red-400 hover:text-red-600 text-xs leading-none ml-2 shrink-0"
title="关闭" title="关闭"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
</div> </div>
+2 -1
View File
@@ -2,6 +2,7 @@
* InviteDialog — input player name, copy invite link * InviteDialog — input player name, copy invite link
*/ */
import { Component, createSignal } from "solid-js"; import { Component, createSignal } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { useJournalStream } from "../stores/journalStream"; import { useJournalStream } from "../stores/journalStream";
interface InviteDialogProps { interface InviteDialogProps {
@@ -51,7 +52,7 @@ export const InviteDialog: Component<InviteDialogProps> = (props) => {
onClick={props.onClose} onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm" class="text-gray-400 hover:text-gray-600 text-sm"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500">
+6 -2
View File
@@ -2,6 +2,10 @@
* JournalHeader — status dot, player name, session dropdown, leave & close * JournalHeader — status dot, player name, session dropdown, leave & close
*/ */
import { Component, Show } from "solid-js"; import { Component, Show } from "solid-js";
import {
TbOutlineArrowLeft,
TbOutlineLayoutSidebarRightCollapse,
} from "solid-icons/tb";
import { useJournalStream, leaveSession } from "../stores/journalStream"; import { useJournalStream, leaveSession } from "../stores/journalStream";
import { SessionDropdown } from "./SessionDropdown"; import { SessionDropdown } from "./SessionDropdown";
@@ -70,7 +74,7 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
class="text-xs text-gray-400 hover:text-blue-500 px-1" class="text-xs text-gray-400 hover:text-blue-500 px-1"
title="返回会话列表" title="返回会话列表"
> >
← <TbOutlineArrowLeft />
</button> </button>
</Show> </Show>
<button <button
@@ -78,7 +82,7 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
class="text-gray-400 hover:text-gray-600 text-sm px-1" class="text-gray-400 hover:text-gray-600 text-sm px-1"
title="隐藏面板" title="隐藏面板"
> >
▸ <TbOutlineLayoutSidebarRightCollapse />
</button> </button>
</div> </div>
</div> </div>
+8 -3
View File
@@ -2,6 +2,11 @@
* SessionDropdown — session switcher for the journal header (GM only) * SessionDropdown — session switcher for the journal header (GM only)
*/ */
import { Component, createSignal, For, Show } from "solid-js"; import { Component, createSignal, For, Show } from "solid-js";
import {
TbOutlineCheck,
TbOutlineChevronDown,
TbOutlineTrash,
} from "solid-icons/tb";
import { import {
sessions, sessions,
joinSession, joinSession,
@@ -46,7 +51,7 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
class="text-[10px] text-gray-400 hover:text-gray-600 truncate leading-tight cursor-pointer text-left" class="text-[10px] text-gray-400 hover:text-gray-600 truncate leading-tight cursor-pointer text-left"
> >
{stream.sessionName || stream.sessionId}{" "} {stream.sessionName || stream.sessionId}{" "}
<span class="text-gray-300">▾</span> <TbOutlineChevronDown class="text-gray-300 inline-block align-[-0.125em]" />
</button> </button>
<Show when={dropdownOpen()}> <Show when={dropdownOpen()}>
<div <div
@@ -71,7 +76,7 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
> >
<span class="truncate flex-1">{meta.name || id}</span> <span class="truncate flex-1">{meta.name || id}</span>
<Show when={id === stream.sessionId}> <Show when={id === stream.sessionId}>
<span class="text-blue-500 shrink-0">✓</span> <TbOutlineCheck class="text-blue-500 shrink-0" />
</Show> </Show>
</button> </button>
<button <button
@@ -83,7 +88,7 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
class="shrink-0 px-2 py-1 text-[10px] text-gray-300 hover:text-red-500" class="shrink-0 px-2 py-1 text-[10px] text-gray-300 hover:text-red-500"
title="删除会话" title="删除会话"
> >
🗑 <TbOutlineTrash />
</button> </button>
</div> </div>
)} )}
+2 -1
View File
@@ -3,6 +3,7 @@
*/ */
import { Component, Show, createMemo } from "solid-js"; import { Component, Show, createMemo } from "solid-js";
import { TbOutlineArrowBackUp } from "solid-icons/tb";
import type { StreamMessage as StreamMessageType } from "./registry"; import type { StreamMessage as StreamMessageType } from "./registry";
import { getMessageType } from "./registry"; import { getMessageType } from "./registry";
import { revertLatest, useJournalStream } from "../stores/journalStream"; import { revertLatest, useJournalStream } from "../stores/journalStream";
@@ -47,7 +48,7 @@ export const StreamMessageCard: Component<{
class="text-xs text-gray-300 hover:text-red-500 ml-auto px-0.5 leading-none" class="text-xs text-gray-300 hover:text-red-500 ml-auto px-0.5 leading-none"
title="撤回此消息" title="撤回此消息"
> >
× <TbOutlineArrowBackUp />
</button> </button>
</Show> </Show>
<span <span
@@ -7,6 +7,7 @@
import type { CompletionItem } from "./command-parser"; import type { CompletionItem } from "./command-parser";
import type { JournalCompletions } from "./completions"; import type { JournalCompletions } from "./completions";
import { TbOutlineDice5, TbOutlineSparkles } from "solid-icons/tb";
export interface CompletionsContext { export interface CompletionsContext {
data: JournalCompletions; data: JournalCompletions;
@@ -63,7 +64,8 @@ export function buildCompletions(
for (const d of diceMatches) { for (const d of diceMatches) {
items.push({ items.push({
label: `🎲 ${d.notation}`, label: d.notation,
icon: TbOutlineDice5,
kind: "value", kind: "value",
insertText: "/roll " + d.notation, insertText: "/roll " + d.notation,
}); });
@@ -71,7 +73,8 @@ export function buildCompletions(
for (const s of sparkMatches) { for (const s of sparkMatches) {
items.push({ items.push({
label: `✨ ${s.slug} (${s.notation})`, label: `${s.slug} (${s.notation})`,
icon: TbOutlineSparkles,
kind: "value", kind: "value",
insertText: `/roll ${s.slug}`, insertText: `/roll ${s.slug}`,
}); });
+4
View File
@@ -5,10 +5,14 @@
* module and keep JournalInput small. * module and keep JournalInput small.
*/ */
import type { IconTypes } from "solid-icons";
export interface CompletionItem { export interface CompletionItem {
label: string; label: string;
kind: "command" | "value" | "no-results"; kind: "command" | "value" | "no-results";
insertText: string; insertText: string;
/** Optional icon rendered before the label in the completions dropdown. */
icon?: IconTypes;
} }
export interface ParsedInput { export interface ParsedInput {
+3 -2
View File
@@ -3,6 +3,7 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { TbOutlineCircleCheck, TbOutlineHandStop } from "solid-icons/tb";
import { registerMessageType } from "../registry"; import { registerMessageType } from "../registry";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -25,7 +26,7 @@ registerMessageType<IntentPayload>({
render: (p) => ( render: (p) => (
<div class="space-y-1"> <div class="space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-lg">🙋</span> <TbOutlineHandStop class="text-lg" />
<span class="text-sm font-medium text-yellow-700">意图</span> <span class="text-sm font-medium text-yellow-700">意图</span>
</div> </div>
<p class="text-sm">{p.description}</p> <p class="text-sm">{p.description}</p>
@@ -58,7 +59,7 @@ registerMessageType<ResolutionPayload>({
render: (p) => ( render: (p) => (
<div class="space-y-1"> <div class="space-y-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-lg">✅</span> <TbOutlineCircleCheck class="text-lg" />
<span class="text-sm font-medium text-green-700">裁决</span> <span class="text-sm font-medium text-green-700">裁决</span>
<span class="text-xs text-gray-400">re: {p.intentId}</span> <span class="text-xs text-gray-400">re: {p.intentId}</span>
</div> </div>
+2 -1
View File
@@ -6,6 +6,7 @@
*/ */
import { Component } from "solid-js"; import { Component } from "solid-js";
import { TbOutlineFileText } from "solid-icons/tb";
import { useNavigateWithParams } from "../../useNavigateWithParams"; import { useNavigateWithParams } from "../../useNavigateWithParams";
import { z } from "zod"; import { z } from "zod";
import { registerMessageType } from "../registry"; import { registerMessageType } from "../registry";
@@ -69,7 +70,7 @@ const RevealLink: Component<LinkPayload> = (p) => {
return ( return (
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-lg">📄</span> <TbOutlineFileText class="text-lg" />
<div> <div>
<a <a
href={target()} href={target()}
+4 -2
View File
@@ -7,6 +7,7 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { TbOutlineArrowRight, TbOutlineDice5 } from "solid-icons/tb";
import { registerMessageType } from "../registry"; import { registerMessageType } from "../registry";
import { rollFormula } from "../../dice"; import { rollFormula } from "../../dice";
@@ -55,9 +56,10 @@ registerMessageType<RollPayload>({
return ( return (
<div class="space-y-0.5"> <div class="space-y-0.5">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="text-lg">🎲</span> <TbOutlineDice5 class="text-lg" />
<span class="font-mono text-sm"> <span class="font-mono text-sm">
{p.notation} →{" "} {p.notation}{" "}
<TbOutlineArrowRight class="inline-block align-[-0.125em]" />{" "}
<span class="font-bold text-base">{result.total}</span> <span class="font-bold text-base">{result.total}</span>
</span> </span>
</div> </div>
+2 -1
View File
@@ -10,6 +10,7 @@
*/ */
import { z } from "zod"; import { z } from "zod";
import { TbOutlineHash } from "solid-icons/tb";
import { registerMessageType } from "../registry"; import { registerMessageType } from "../registry";
import { journalSetState } from "../../stores/journalStream"; import { journalSetState } from "../../stores/journalStream";
import { produce } from "solid-js/store"; import { produce } from "solid-js/store";
@@ -35,7 +36,7 @@ registerMessageType<VarPayload>({
render: (p) => { render: (p) => {
return ( return (
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="text-lg">🔢</span> <TbOutlineHash class="text-lg" />
<span class="font-mono text-sm"> <span class="font-mono text-sm">
{p.key} = <span class="font-bold">{p.value}</span> {p.key} = <span class="font-bold">{p.value}</span>
</span> </span>
+3 -2
View File
@@ -1,4 +1,5 @@
import { For, Show, createEffect, createSignal, on } from "solid-js"; import { For, Show, createEffect, createSignal, on } from "solid-js";
import { TbOutlineChevronLeft, TbOutlineChevronRight } from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore"; import type { DeckStore } from "./hooks/deckStore";
export interface CardListProps { export interface CardListProps {
@@ -67,7 +68,7 @@ export function CardList(props: CardListProps) {
disabled={page() === 0} disabled={page() === 0}
class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer" class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
> >
‹ <TbOutlineChevronLeft />
</button> </button>
<span class="text-xs text-gray-500 tabular-nums"> <span class="text-xs text-gray-500 tabular-nums">
{page() + 1} / {totalPages()} {page() + 1} / {totalPages()}
@@ -77,7 +78,7 @@ export function CardList(props: CardListProps) {
disabled={page() >= totalPages() - 1} disabled={page() >= totalPages() - 1}
class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer" class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
> >
› <TbOutlineChevronRight />
</button> </button>
</div> </div>
</Show> </Show>
+17 -3
View File
@@ -1,5 +1,11 @@
import { Show, createSignal, onMount } from "solid-js"; import { Show, createSignal, onMount } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import {
TbOutlineCheck,
TbOutlineCopy,
TbOutlineDownload,
TbOutlinePencil,
} from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore"; import type { DeckStore } from "./hooks/deckStore";
export interface DeckHeaderProps { export interface DeckHeaderProps {
@@ -43,7 +49,15 @@ export function DeckHeader(props: DeckHeaderProps) {
: "bg-gray-100 text-gray-600 hover:bg-gray-200" : "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`} }`}
> >
{store.state.isEditing ? "✓ 编辑中" : "✏️ 编辑"} {store.state.isEditing ? (
<>
<TbOutlineCheck class="inline-block align-[-0.125em]" /> 编辑中
</>
) : (
<>
<TbOutlinePencil class="inline-block align-[-0.125em]" /> 编辑
</>
)}
</button> </button>
{/* 导出 PDF 按钮 */} {/* 导出 PDF 按钮 */}
@@ -51,7 +65,7 @@ export function DeckHeader(props: DeckHeaderProps) {
onClick={() => store.actions.exportDeck()} onClick={() => store.actions.exportDeck()}
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-green-100 text-green-600 hover:bg-green-200" class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-green-100 text-green-600 hover:bg-green-200"
> >
📥 导出 PDF <TbOutlineDownload class="inline-block align-[-0.125em]" /> 导出 PDF
</button> </button>
{/* 复制代码按钮 */} {/* 复制代码按钮 */}
@@ -59,7 +73,7 @@ export function DeckHeader(props: DeckHeaderProps) {
onClick={handleCopy} onClick={handleCopy}
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-purple-100 text-purple-600 hover:bg-purple-200" class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-purple-100 text-purple-600 hover:bg-purple-200"
> >
📋 复制代码 <TbOutlineCopy class="inline-block align-[-0.125em]" /> 复制代码
</button> </button>
</div> </div>
+2 -1
View File
@@ -1,5 +1,6 @@
import { createSignal, For, Show, createMemo } from "solid-js"; import { createSignal, For, Show, createMemo } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { TbOutlineDownload } from "solid-icons/tb";
import { parsePlt, extractCutPaths } from "../../plotcutter/parser"; import { parsePlt, extractCutPaths } from "../../plotcutter/parser";
import { generateTravelPaths, travelPathsToSvg } from "../../plotcutter/layout"; import { generateTravelPaths, travelPathsToSvg } from "../../plotcutter/layout";
import type { CardPath } from "../../plotcutter/types"; import type { CardPath } from "../../plotcutter/types";
@@ -115,7 +116,7 @@ export function PltPreview(props: PltPreviewProps) {
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-1" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-1"
disabled={parsedData().cardPaths.length === 0} disabled={parsedData().cardPaths.length === 0}
> >
📥 下载 PLT <TbOutlineDownload /> 下载 PLT
</button> </button>
<button <button
@@ -1,4 +1,5 @@
import { Show } from 'solid-js'; import { Show } from 'solid-js';
import { TbOutlineCircleX, TbOutlineX } from 'solid-icons/tb';
import type { DeckStore } from './hooks/deckStore'; import type { DeckStore } from './hooks/deckStore';
export interface PrintPreviewFooterProps { export interface PrintPreviewFooterProps {
@@ -26,14 +27,14 @@ export function PrintPreviewFooter(props: PrintPreviewFooterProps) {
<Show when={store.state.exportError}> <Show when={store.state.exportError}>
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2 text-red-600"> <div class="flex items-center gap-2 text-red-600">
<span>❌</span> <TbOutlineCircleX />
<span class="text-sm font-medium">{store.state.exportError}</span> <span class="text-sm font-medium">{store.state.exportError}</span>
</div> </div>
<button <button
onClick={() => store.actions.clearExportError()} onClick={() => store.actions.clearExportError()}
class="text-gray-500 hover:text-gray-700 cursor-pointer" class="text-gray-500 hover:text-gray-700 cursor-pointer"
> >
✕ <TbOutlineX />
</button> </button>
</div> </div>
</Show> </Show>
@@ -1,4 +1,9 @@
import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
import {
TbOutlineDownload,
TbOutlinePlus,
TbOutlineRuler,
} from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore"; import type { DeckStore } from "./hooks/deckStore";
import { getCompatibleDecks } from "./hooks/deck-registry"; import { getCompatibleDecks } from "./hooks/deck-registry";
@@ -77,7 +82,7 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {
: "添加其他牌组到打印" : "添加其他牌组到打印"
} }
> >
<span>➕</span> <TbOutlinePlus />
<span>添加牌组</span> <span>添加牌组</span>
<Show when={props.selectedDeckIds.length > 0}> <Show when={props.selectedDeckIds.length > 0}>
<span class="bg-purple-600 text-white text-xs rounded-full w-5 h-5 inline-flex items-center justify-center"> <span class="bg-purple-600 text-white text-xs rounded-full w-5 h-5 inline-flex items-center justify-center">
@@ -232,14 +237,14 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {
onClick={props.onOpenPltPreview} onClick={props.onOpenPltPreview}
class="bg-green-600 hover:bg-green-700 text-white px-4 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-2" class="bg-green-600 hover:bg-green-700 text-white px-4 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-2"
> >
<span>📐</span> <TbOutlineRuler />
<span>PLT 预览</span> <span>PLT 预览</span>
</button> </button>
<button <button
onClick={props.onExport} onClick={props.onExport}
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-2" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1.5 rounded text-sm font-medium cursor-pointer flex items-center gap-2"
> >
<span>📥</span> <TbOutlineDownload />
<span>导出 PDF</span> <span>导出 PDF</span>
</button> </button>
<button <button
@@ -1,10 +1,27 @@
import { For, createSignal, onCleanup, onMount } from "solid-js"; import { For, createSignal, onCleanup, onMount } from "solid-js";
import type { JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import { createSortable, useDragDropContext } from "@thisbeyond/solid-dnd"; import { createSortable, useDragDropContext } from "@thisbeyond/solid-dnd";
import type { IconTypes } from "solid-icons";
import {
TbOutlineAlignCenter,
TbOutlineAlignLeft,
TbOutlineAlignRight,
TbOutlineArrowDown,
TbOutlineArrowDownLeft,
TbOutlineArrowDownRight,
TbOutlineArrowLeft,
TbOutlineArrowRight,
TbOutlineArrowUp,
TbOutlineArrowUpLeft,
TbOutlineArrowUpRight,
TbOutlineEye,
TbOutlineEyeOff,
TbOutlineGripVertical,
TbOutlineTrash,
} from "solid-icons/tb";
import type { DeckStore } from "../hooks/deckStore"; import type { DeckStore } from "../hooks/deckStore";
import type { Align, LayerConfig } from "../types"; import type { Align, LayerConfig } from "../types";
import alignLeftIcon from "./icons/align-left.png";
import alignCenterIcon from "./icons/align-center.png";
import alignRightIcon from "./icons/align-right.png";
export interface LayerRowProps { export interface LayerRowProps {
store: DeckStore; store: DeckStore;
@@ -22,65 +39,67 @@ export interface LayerRowProps {
onRemove: () => void; onRemove: () => void;
} }
const ORIENTATIONS = [ const ORIENTATIONS: {
{ value: "n" as const, label: "↑ 北" }, value: "n" | "e" | "s" | "w";
{ value: "e" as const, label: "→ 东" }, label: string;
{ value: "s" as const, label: "↓ 南" }, icon: IconTypes;
{ value: "w" as const, label: "← 西" }, }[] = [
{ value: "n", label: "北", icon: TbOutlineArrowUp },
{ value: "e", label: "东", icon: TbOutlineArrowRight },
{ value: "s", label: "南", icon: TbOutlineArrowDown },
{ value: "w", label: "西", icon: TbOutlineArrowLeft },
]; ];
const ALIGNS: { value: Align | ""; icon: string }[] = [ const ALIGNS: { value: Align | ""; icon: IconTypes }[] = [
{ value: "", icon: alignCenterIcon }, { value: "", icon: TbOutlineAlignCenter },
{ value: "l", icon: alignLeftIcon }, { value: "l", icon: TbOutlineAlignLeft },
{ value: "c", icon: alignCenterIcon }, { value: "c", icon: TbOutlineAlignCenter },
{ value: "r", icon: alignRightIcon }, { value: "r", icon: TbOutlineAlignRight },
{ value: "tl", icon: "↖" }, { value: "tl", icon: TbOutlineArrowUpLeft },
{ value: "tc", icon: "↑" }, { value: "tc", icon: TbOutlineArrowUp },
{ value: "tr", icon: "↗" }, { value: "tr", icon: TbOutlineArrowUpRight },
{ value: "bl", icon: "↙" }, { value: "bl", icon: TbOutlineArrowDownLeft },
{ value: "bc", icon: "↓" }, { value: "bc", icon: TbOutlineArrowDown },
{ value: "br", icon: "↘" }, { value: "br", icon: TbOutlineArrowDownRight },
]; ];
const FONT_PRESETS = [3, 5, 8, 12] as const; const FONT_PRESETS = [3, 5, 8, 12] as const;
function orientChar(v: string) { function orientIcon(v: string): IconTypes {
switch (v) { switch (v) {
case "n": case "n":
return "↑"; return TbOutlineArrowUp;
case "e": case "e":
return "→"; return TbOutlineArrowRight;
case "s": case "s":
return "↓"; return TbOutlineArrowDown;
case "w": case "w":
return "←"; return TbOutlineArrowLeft;
default: default:
return "↑"; return TbOutlineArrowUp;
} }
} }
function alignIcon(v: string): import("solid-js").JSX.Element { function alignIcon(v: string): IconTypes {
switch (v) { switch (v) {
case "tl": case "tl":
return "↖"; return TbOutlineArrowUpLeft;
case "tc": case "tc":
return "↑"; return TbOutlineArrowUp;
case "tr": case "tr":
return "↗"; return TbOutlineArrowUpRight;
case "bl": case "bl":
return "↙"; return TbOutlineArrowDownLeft;
case "bc": case "bc":
return "↓"; return TbOutlineArrowDown;
case "br": case "br":
return "↘"; return TbOutlineArrowDownRight;
case "l": case "l":
return <img src={alignLeftIcon} alt="align" class="w-5 h-5 not-prose" />; return TbOutlineAlignLeft;
case "r": case "r":
return <img src={alignRightIcon} alt="align" class="w-5 h-5 not-prose" />; return TbOutlineAlignRight;
default: default:
return ( return TbOutlineAlignCenter;
<img src={alignCenterIcon} alt="align" class="w-5 h-5 not-prose" />
);
} }
} }
@@ -125,7 +144,7 @@ export function LayerRow(props: LayerRowProps) {
class="cursor-grab text-gray-400 hover:text-gray-600 text-sm px-0.5 select-none shrink-0" class="cursor-grab text-gray-400 hover:text-gray-600 text-sm px-0.5 select-none shrink-0"
title="拖动排序" title="拖动排序"
> >
⠿ <TbOutlineGripVertical class="inline-block align-[-0.125em]" />
</button> </button>
<button <button
@@ -133,7 +152,11 @@ export function LayerRow(props: LayerRowProps) {
class="shrink-0 text-sm cursor-pointer select-none" class="shrink-0 text-sm cursor-pointer select-none"
title={props.layer.visible ? "隐藏" : "显示"} title={props.layer.visible ? "隐藏" : "显示"}
> >
{props.layer.visible ? "👁" : "👁‍🗨"} {props.layer.visible ? (
<TbOutlineEye class="inline-block align-[-0.125em]" />
) : (
<TbOutlineEyeOff class="inline-block align-[-0.125em]" />
)}
</button> </button>
<span <span
@@ -144,7 +167,7 @@ export function LayerRow(props: LayerRowProps) {
</span> </span>
<DropdownButton <DropdownButton
icon={orientChar(props.layer.orientation || "n")} icon={orientIcon(props.layer.orientation || "n")}
visible={props.layer.visible} visible={props.layer.visible}
open={props.openDropdown === `orient-${props.index}`} open={props.openDropdown === `orient-${props.index}`}
onToggle={() => onToggle={() =>
@@ -162,6 +185,10 @@ export function LayerRow(props: LayerRowProps) {
onClick={() => props.onUpdateOrientation(o.value)} onClick={() => props.onUpdateOrientation(o.value)}
class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap" class="block w-full text-left px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
> >
<Dynamic
component={o.icon}
class="inline-block align-[-0.125em]"
/>{" "}
{o.label} {o.label}
</button> </button>
)} )}
@@ -187,13 +214,11 @@ export function LayerRow(props: LayerRowProps) {
onClick={() => props.onUpdateAlign(o.value || undefined)} onClick={() => props.onUpdateAlign(o.value || undefined)}
class="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap" class="flex items-center gap-2 w-full px-3 py-1.5 text-sm hover:bg-gray-100 cursor-pointer whitespace-nowrap"
> >
{o.icon.endsWith(".png") ? ( <Dynamic
<img src={o.icon} alt="" class="w-4 h-4 not-prose max-w-none" /> component={o.icon}
) : ( size={16}
<span class="w-4 h-4 flex items-center justify-center not-prose"> class="not-prose max-w-none"
{o.icon} />
</span>
)}
</button> </button>
)} )}
</For> </For>
@@ -260,19 +285,19 @@ export function LayerRow(props: LayerRowProps) {
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"}`} 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="删除图层" title="删除图层"
> >
✕ <TbOutlineTrash size={14} />
</button> </button>
</div> </div>
); );
} }
function DropdownButton(props: { function DropdownButton(props: {
icon: import("solid-js").JSX.Element; icon: IconTypes;
visible: boolean; visible: boolean;
open: boolean; open: boolean;
onToggle: () => void; onToggle: () => void;
title: string; title: string;
children: import("solid-js").JSX.Element; children: JSX.Element;
}) { }) {
return ( return (
<div class="relative"> <div class="relative">
@@ -284,7 +309,7 @@ function DropdownButton(props: {
class={`w-7 h-7 rounded cursor-pointer flex items-center justify-center bg-gray-200 hover:bg-gray-300 ${!props.visible ? "invisible pointer-events-none" : ""}`} class={`w-7 h-7 rounded cursor-pointer flex items-center justify-center bg-gray-200 hover:bg-gray-300 ${!props.visible ? "invisible pointer-events-none" : ""}`}
title={props.title} title={props.title}
> >
{props.icon} <Dynamic component={props.icon} size={16} />
</button> </button>
{props.open && ( {props.open && (
<div <div
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1002 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 B

+2 -1
View File
@@ -1,5 +1,6 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import { createSignal, onMount } from "solid-js"; import { createSignal, onMount } from "solid-js";
import { TbOutlineDice5 } from "solid-icons/tb";
import { rollFormula } from "./dice"; import { rollFormula } from "./dice";
export interface DiceProps { export interface DiceProps {
@@ -86,7 +87,7 @@ customElement("md-dice", { key: "" }, (props, { element }) => {
class="text-blue-600 hover:text-blue-800 cursor-pointer" class="text-blue-600 hover:text-blue-800 cursor-pointer"
title={rollDetail() || "掷骰子"} title={rollDetail() || "掷骰子"}
> >
🎲 <TbOutlineDice5 class="inline-block align-[-0.125em]" />
</span> </span>
<a <a
onClick={handleRoll} onClick={handleRoll}
+2 -1
View File
@@ -1,5 +1,6 @@
import { customElement, noShadowDOM } from "solid-element"; import { customElement, noShadowDOM } from "solid-element";
import {Show, For, createResource, createMemo, createSignal} from "solid-js"; import {Show, For, createResource, createMemo, createSignal} from "solid-js";
import { TbOutlineCopy } from "solid-icons/tb";
import { resolvePath } from "./utils/path"; import { resolvePath } from "./utils/path";
import { createPinsStore } from "./stores/pinsStore"; import { createPinsStore } from "./stores/pinsStore";
@@ -108,7 +109,7 @@ customElement("md-pins", { pins: "", fixed: false, labelStart: "A" }, (props, {
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" 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 坐标" title="复制所有 pin 坐标"
> >
📋 复制 <TbOutlineCopy /> 复制
</button> </button>
</div> </div>
</Show> </Show>
+2 -1
View File
@@ -7,6 +7,7 @@ import {
createMemo, createMemo,
createResource, createResource,
} from "solid-js"; } from "solid-js";
import { TbOutlineDice5 } from "solid-icons/tb";
import { parseMarkdown } from "../markdown"; import { parseMarkdown } from "../markdown";
import { parseCSVString, CSV, processVariables } from "./utils/csv-loader"; import { parseCSVString, CSV, processVariables } from "./utils/csv-loader";
import { resolveContentRef } from "./utils/resolve-content"; import { resolveContentRef } from "./utils/resolve-content";
@@ -210,7 +211,7 @@ customElement(
class="text-gray-500 hover:text-gray-700 shrink-0 cursor-pointer" class="text-gray-500 hover:text-gray-700 shrink-0 cursor-pointer"
title="随机切换" title="随机切换"
> >
🎲 <TbOutlineDice5 class="inline-block align-[-0.125em]" />
</button> </button>
</Show> </Show>
<div <div
+8 -3
View File
@@ -1,5 +1,10 @@
import { customElement, noShadowDOM } from 'solid-element'; import { customElement, noShadowDOM } from 'solid-element';
import { For, Show, createEffect } from 'solid-js'; import { For, Show, createEffect } from 'solid-js';
import {
TbOutlineRotate,
TbOutlinePlayerTrackNext,
TbOutlineArrowRight,
} from 'solid-icons/tb';
import type {TextResult, RuntimeResult, OptionsResult} from 'yarn-spinner-loader'; import type {TextResult, RuntimeResult, OptionsResult} from 'yarn-spinner-loader';
import { createYarnStore } from './stores/yarnStore'; import { createYarnStore } from './stores/yarnStore';
@@ -82,7 +87,7 @@ customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { el
rounded-bl-md transition-colors cursor-pointer" rounded-bl-md transition-colors cursor-pointer"
title="重新开始" title="重新开始"
> >
🔄 <TbOutlineRotate class="inline-block align-[-0.125em]" />
</button> </button>
<button <button
onClick={() => advance()} onClick={() => advance()}
@@ -90,7 +95,7 @@ customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { el
transition-colors cursor-pointer" transition-colors cursor-pointer"
title="继续" title="继续"
> >
⏩ <TbOutlinePlayerTrackNext class="inline-block align-[-0.125em]" />
</button> </button>
</div> </div>
@@ -104,7 +109,7 @@ customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { el
class="option-button text-left px-4 py-2 bg-blue-50 hover:bg-blue-100 class="option-button text-left px-4 py-2 bg-blue-50 hover:bg-blue-100
rounded border border-blue-200 transition-colors cursor-pointer" rounded border border-blue-200 transition-colors cursor-pointer"
> >
→ {option.text} <TbOutlineArrowRight class="inline-block align-[-0.125em]" /> {option.text}
</button> </button>
)} )}
</For> </For>
+3 -3
View File
@@ -1,16 +1,16 @@
--- ---
tag: journal-gm tag: journal-gm
icon: 🎭 icon: gm
title: 主持人指南 title: 主持人指南
--- ---
作为 GM 创建会话、管理玩家、使用命令与玩家互动。 作为 GM 创建会话、管理玩家、使用命令与玩家互动。
点击顶部导航栏的 📋 按钮打开 Journal 面板。 点击顶部导航栏的剪贴板按钮打开 Journal 面板。
## 快速开始 ## 快速开始
1. 点击顶部导航栏的 📋 按钮打开 Journal 面板 1. 点击顶部导航栏的剪贴板按钮打开 Journal 面板
2. 输入你的名字,选择"主持人"角色,点击"加入" 2. 输入你的名字,选择"主持人"角色,点击"加入"
3. 连接成功后,你可以创建新会话或选择已有会话 3. 连接成功后,你可以创建新会话或选择已有会话
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: journal-link tag: journal-link
icon: 🔗 icon: link
title: 链接命令 title: 链接命令
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
tag: journal-player tag: journal-player
icon: 🎲 icon: player
title: 玩家指南 title: 玩家指南
--- ---
@@ -19,7 +19,7 @@ GM 会发送一个包含 `?session=xxx&player=你的名字&autojoin=1` 的链接
### 手动加入 ### 手动加入
1. 点击顶部导航栏的 📋 按钮打开 Journal 面板 1. 点击顶部导航栏的剪贴板按钮打开 Journal 面板
2. 输入你的名字 2. 输入你的名字
3. 选择"玩家"角色 3. 选择"玩家"角色
4. 点击"加入" 4. 点击"加入"
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: journal-roll tag: journal-roll
icon: 🎲 icon: roll
title: 掷骰与火花表命令 title: 掷骰与火花表命令
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: journal-set tag: journal-set
icon: 🔢 icon: set
title: 变量系统 title: 变量系统
description: 在文档中声明变量和标签,通过 /set 命令设置变量值,自动计算派生变量和标签效果。 description: 在文档中声明变量和标签,通过 /set 命令设置变量值,自动计算派生变量和标签效果。
syntax: '```yaml role=declare' syntax: '```yaml role=declare'
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: journal-spark tag: journal-spark
icon: 🎰 icon: spark
title: 火花表 title: 火花表
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-bg tag: md-bg
icon: 🖼️ icon: background
title: 背景组件 title: 背景组件
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-border tag: md-border
icon: 🖼️ icon: border
title: 边框组件 title: 边框组件
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-deck tag: md-deck
icon: 🃏 icon: deck
title: 卡牌组件 title: 卡牌组件
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-dice tag: md-dice
icon: 🎲 icon: dice
title: 骰子组件 title: 骰子组件
--- ---
@@ -23,7 +23,7 @@ title: 骰子组件
:md-dice[2d20k1+5]{key="advantage"} :md-dice[2d20k1+5]{key="advantage"}
``` ```
点击 🎲 图标执行投掷,显示结果;点击文字重置为原始公式。 点击骰子图标执行投掷,显示结果;点击文字重置为原始公式。
## 属性 ## 属性
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-embed tag: md-embed
icon: 📄 icon: embed
title: 嵌入组件 title: 嵌入组件
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-font tag: md-font
icon: 🔤 icon: font
title: 字体组件 title: 字体组件
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-link tag: md-link
icon: 🔗 icon: link
title: 链接组件 title: 链接组件
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-pins tag: md-pins
icon: 📍 icon: pins
title: 标记组件 title: 标记组件
--- ---
@@ -27,7 +27,7 @@ title: 标记组件
## 复制坐标 ## 复制坐标
非 `fixed` 模式下,图片右上角会显示 📋 复制按钮,点击可将所有标记的坐标复制到剪贴板,方便粘贴回文档的 `pins` 属性中。 非 `fixed` 模式下,图片右上角会显示复制按钮,点击可将所有标记的坐标复制到剪贴板,方便粘贴回文档的 `pins` 属性中。
## 属性 ## 属性
+2 -2
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-table tag: md-table
icon: 📊 icon: table
title: 表格组件 title: 表格组件
--- ---
@@ -38,7 +38,7 @@ B,另一段描述,第二章]{}
## 随机抽取 ## 随机抽取
当 `roll=true` 时,表格顶部显示 🎲 随机按钮: 当 `roll=true` 时,表格顶部显示骰子随机按钮:
- 普通随机:从所有行中均匀随机选取 - 普通随机:从所有行中均匀随机选取
- **加权随机**:当 `label` 列全部为整数或整数范围格式(如 `1-3`)时,以 label 值为权重进行加权随机抽取 - **加权随机**:当 `label` 列全部为整数或整数范围格式(如 `1-3`)时,以 label 值为权重进行加权随机抽取
+3 -3
View File
@@ -1,6 +1,6 @@
--- ---
tag: md-yarn-spinner tag: md-yarn-spinner
icon: 🧶 icon: yarn
title: 叙事线组件 title: 叙事线组件
--- ---
@@ -19,8 +19,8 @@ Yarn Spinner 是用于游戏对话系统的格式,支持选项、条件分支
- **对话历史**:上半部分显示已进行的对话,说话者以蓝色粗体显示,命令以灰色斜体显示 - **对话历史**:上半部分显示已进行的对话,说话者以蓝色粗体显示,命令以灰色斜体显示
- **当前选项**:下半部分显示可选的对话选项,点击选项推进剧情 - **当前选项**:下半部分显示可选的对话选项,点击选项推进剧情
- **⏩ 继续**:点击右上角继续按钮推进到下一段对话 - **继续**:点击右上角继续按钮推进到下一段对话
- **🔄 重新开始**:点击右上角重启按钮从头开始对话 - **重新开始**:点击右上角重启按钮从头开始对话
## 属性 ## 属性