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
+24 -8
View File
@@ -6,8 +6,17 @@ import {
onCleanup,
onMount,
} 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 { journalDocEntries, type JournalDocEntry } from "./journal-doc-data";
import { docIcon } from "./doc-icons";
import { parseMarkdown } from "../markdown";
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"
>
{docSetLabel()}
<span class="text-gray-400 text-sm">▾</span>
<TbOutlineChevronDown class="text-gray-400 text-sm" />
</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-60 py-1">
@@ -89,10 +98,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "text-gray-700"
}`}
>
<span>📖</span>
<TbOutlineBook2 />
<span>指令组件文档</span>
<Show when={docSet() === "directives"}>
<span class="ml-auto text-blue-500">✓</span>
<TbOutlineCheck class="ml-auto text-blue-500" />
</Show>
</button>
<button
@@ -103,10 +112,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "text-gray-700"
}`}
>
<span>📋</span>
<TbOutlineClipboardList />
<span>Journal 服务器文档</span>
<Show when={docSet() === "journal"}>
<span class="ml-auto text-blue-500">✓</span>
<TbOutlineCheck class="ml-auto text-blue-500" />
</Show>
</button>
</div>
@@ -117,7 +126,7 @@ const DocDialog: Component<DocDialogProps> = (props) => {
class="text-gray-400 hover:text-gray-600 text-xl leading-none p-1"
title="关闭"
>
✕
<TbOutlineX />
</button>
</div>
@@ -133,7 +142,10 @@ const DocDialog: Component<DocDialogProps> = (props) => {
: "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
when={docSet() === "directives"}
fallback={<span class="text-xs">{entry.title}</span>}
@@ -164,7 +176,11 @@ const DocContent: Component<{ entry: AnyEntry }> = (props) => {
return (
<div class="max-w-none">
<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>
<article
+13 -3
View File
@@ -1,5 +1,11 @@
import { Component, createMemo, createSignal, Show } from "solid-js";
import { useLocation } from "@solidjs/router";
import {
TbOutlineFolderOpen,
TbOutlineFolder,
TbOutlineFile,
TbOutlineChevronRight,
} from "solid-icons/tb";
import { type FileNode, type TocNode } from "../data-loader";
import { useNavigateWithParams } from "./useNavigateWithParams";
import { useScrollContainer } from "../App";
@@ -61,10 +67,14 @@ export const FileTreeNode: Component<{
onClick={handleClick}
>
<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 when={!isDir}>
<span class="mr-1 text-gray-400">📄</span>
<TbOutlineFile class="mr-1 text-gray-400" />
</Show>
<span class="text-sm truncate">{props.node.name}</span>
</a>
@@ -151,7 +161,7 @@ export const HeadingNode: Component<{
<span
class={`${hasChildren ? "" : "invisible"} ${isExpanded() ? "rotate-90" : ""} mt-1 inline-block transition-transform `}
>
▶
<TbOutlineChevronRight class="inline-block align-[-0.125em]" />
</span>
</span>
<a
+9 -25
View File
@@ -15,8 +15,10 @@ import {
onCleanup,
Show,
} from "solid-js";
import { Portal } from "solid-js/web";
import { Portal, Dynamic } from "solid-js/web";
import { useLocation } from "@solidjs/router";
import type { IconTypes } from "solid-icons";
import { TbOutlineLink, TbOutlineSparkles } from "solid-icons/tb";
import { useArticleDom } from "./Article";
import { journalStreamState } from "./stores/journalStream";
import { useJournalCompletions } from "./journal/completions";
@@ -27,25 +29,6 @@ import {
normalizePath,
} 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
// ---------------------------------------------------------------------------
@@ -54,7 +37,7 @@ interface FloatingButton {
rect: DOMRect;
action: () => void;
title: string;
svg: string;
icon: IconTypes;
color: string;
}
@@ -130,7 +113,7 @@ export const RevealManager: Component = () => {
text: combinedSlug,
}),
title: "Roll spark table",
svg: SPARK_SVG,
icon: TbOutlineSparkles,
color: "text-purple-400 hover:text-purple-600 hover:bg-purple-50",
});
return;
@@ -150,7 +133,7 @@ export const RevealManager: Component = () => {
text: `${normalized}#${text}`,
}),
title: "Send /link to stream",
svg: LINK_SVG,
icon: TbOutlineLink,
color: "text-blue-500 hover:bg-blue-50",
});
}
@@ -183,14 +166,15 @@ export const RevealManager: Component = () => {
left: `${btn().rect.left - 20}px`,
}}
title={btn().title}
innerHTML={btn().svg}
onClick={(e) => {
e.stopPropagation();
btn().action();
}}
onMouseEnter={() => clearTimeout(hideTimer)}
onMouseLeave={hide}
/>
>
<Dynamic component={btn().icon} size={12} />
</button>
</Portal>
)}
</Show>
+3 -2
View File
@@ -1,6 +1,7 @@
import { Component, createMemo, createSignal, onMount, Show } from "solid-js";
import { generateToc, type FileNode, type TocNode } from "../data-loader";
import { useLocation } from "@solidjs/router";
import { TbOutlineFolderOpen, TbOutlineX } from "solid-icons/tb";
import { FileTreeNode, HeadingNode } from "./FileTree";
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"
title="Content Source"
>
📂
<TbOutlineFolderOpen class="inline-block align-[-0.125em]" />
</button>
<Show when={!props.isDesktop}>
<button
@@ -65,7 +66,7 @@ const SidebarContent: Component<SidebarContentProps> = (props) => {
class="text-gray-500 hover:text-gray-700"
title="关闭"
>
✕
<TbOutlineX class="inline-block align-[-0.125em]" />
</button>
</Show>
</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 { Dynamic } from "solid-js/web";
import type { CompletionItem } from "./command-parser";
import type { CompletionsState } from "./completions";
@@ -102,6 +103,11 @@ export const CompletionsDropdown: Component<CompletionsDropdownProps> = (
{item.kind === "command" ? "cmd" : "val"}
</span>
</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>
</div>
)}
+2 -1
View File
@@ -6,6 +6,7 @@
* or creating a session is a separate step that reuses that connection.
*/
import { Component, createSignal, For, onMount, Show } from "solid-js";
import { TbOutlineTrash } from "solid-icons/tb";
import {
connectBroker,
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"
title="删除会话"
>
🗑
<TbOutlineTrash />
</button>
</Show>
</div>
@@ -2,6 +2,7 @@
* CreateSessionDialog — modal for naming a new session
*/
import { Component, createSignal, onMount } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { createSession, joinSession } from "../stores/journalStream";
interface CreateSessionDialogProps {
@@ -48,7 +49,7 @@ export const CreateSessionDialog: Component<CreateSessionDialogProps> = (
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm"
>
✕
<TbOutlineX />
</button>
</div>
<p class="text-xs text-gray-500">输入新会话的名称。</p>
@@ -5,6 +5,7 @@
* log), so the warning spells out that it cannot be undone.
*/
import { Component, createSignal, Show } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { deleteSession } from "../stores/journalStream";
interface DeleteSessionDialogProps {
@@ -49,7 +50,7 @@ export const DeleteSessionDialog: Component<DeleteSessionDialogProps> = (
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm"
>
✕
<TbOutlineX />
</button>
</div>
<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.
*/
import { Component, Show } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
interface ErrorPopupProps {
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"
title="关闭"
>
✕
<TbOutlineX />
</button>
</div>
</div>
+2 -1
View File
@@ -2,6 +2,7 @@
* InviteDialog — input player name, copy invite link
*/
import { Component, createSignal } from "solid-js";
import { TbOutlineX } from "solid-icons/tb";
import { useJournalStream } from "../stores/journalStream";
interface InviteDialogProps {
@@ -51,7 +52,7 @@ export const InviteDialog: Component<InviteDialogProps> = (props) => {
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm"
>
✕
<TbOutlineX />
</button>
</div>
<p class="text-xs text-gray-500">
+6 -2
View File
@@ -2,6 +2,10 @@
* JournalHeader — status dot, player name, session dropdown, leave & close
*/
import { Component, Show } from "solid-js";
import {
TbOutlineArrowLeft,
TbOutlineLayoutSidebarRightCollapse,
} from "solid-icons/tb";
import { useJournalStream, leaveSession } from "../stores/journalStream";
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"
title="返回会话列表"
>
←
<TbOutlineArrowLeft />
</button>
</Show>
<button
@@ -78,7 +82,7 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
class="text-gray-400 hover:text-gray-600 text-sm px-1"
title="隐藏面板"
>
▸
<TbOutlineLayoutSidebarRightCollapse />
</button>
</div>
</div>
+8 -3
View File
@@ -2,6 +2,11 @@
* SessionDropdown — session switcher for the journal header (GM only)
*/
import { Component, createSignal, For, Show } from "solid-js";
import {
TbOutlineCheck,
TbOutlineChevronDown,
TbOutlineTrash,
} from "solid-icons/tb";
import {
sessions,
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"
>
{stream.sessionName || stream.sessionId}{" "}
<span class="text-gray-300">▾</span>
<TbOutlineChevronDown class="text-gray-300 inline-block align-[-0.125em]" />
</button>
<Show when={dropdownOpen()}>
<div
@@ -71,7 +76,7 @@ export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
>
<span class="truncate flex-1">{meta.name || id}</span>
<Show when={id === stream.sessionId}>
<span class="text-blue-500 shrink-0">✓</span>
<TbOutlineCheck class="text-blue-500 shrink-0" />
</Show>
</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"
title="删除会话"
>
🗑
<TbOutlineTrash />
</button>
</div>
)}
+2 -1
View File
@@ -3,6 +3,7 @@
*/
import { Component, Show, createMemo } from "solid-js";
import { TbOutlineArrowBackUp } from "solid-icons/tb";
import type { StreamMessage as StreamMessageType } from "./registry";
import { getMessageType } from "./registry";
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"
title="撤回此消息"
>
×
<TbOutlineArrowBackUp />
</button>
</Show>
<span
@@ -7,6 +7,7 @@
import type { CompletionItem } from "./command-parser";
import type { JournalCompletions } from "./completions";
import { TbOutlineDice5, TbOutlineSparkles } from "solid-icons/tb";
export interface CompletionsContext {
data: JournalCompletions;
@@ -63,7 +64,8 @@ export function buildCompletions(
for (const d of diceMatches) {
items.push({
label: `🎲 ${d.notation}`,
label: d.notation,
icon: TbOutlineDice5,
kind: "value",
insertText: "/roll " + d.notation,
});
@@ -71,7 +73,8 @@ export function buildCompletions(
for (const s of sparkMatches) {
items.push({
label: `✨ ${s.slug} (${s.notation})`,
label: `${s.slug} (${s.notation})`,
icon: TbOutlineSparkles,
kind: "value",
insertText: `/roll ${s.slug}`,
});
+4
View File
@@ -5,10 +5,14 @@
* module and keep JournalInput small.
*/
import type { IconTypes } from "solid-icons";
export interface CompletionItem {
label: string;
kind: "command" | "value" | "no-results";
insertText: string;
/** Optional icon rendered before the label in the completions dropdown. */
icon?: IconTypes;
}
export interface ParsedInput {
+3 -2
View File
@@ -3,6 +3,7 @@
*/
import { z } from "zod";
import { TbOutlineCircleCheck, TbOutlineHandStop } from "solid-icons/tb";
import { registerMessageType } from "../registry";
// ---------------------------------------------------------------------------
@@ -25,7 +26,7 @@ registerMessageType<IntentPayload>({
render: (p) => (
<div class="space-y-1">
<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>
</div>
<p class="text-sm">{p.description}</p>
@@ -58,7 +59,7 @@ registerMessageType<ResolutionPayload>({
render: (p) => (
<div class="space-y-1">
<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-xs text-gray-400">re: {p.intentId}</span>
</div>
+2 -1
View File
@@ -6,6 +6,7 @@
*/
import { Component } from "solid-js";
import { TbOutlineFileText } from "solid-icons/tb";
import { useNavigateWithParams } from "../../useNavigateWithParams";
import { z } from "zod";
import { registerMessageType } from "../registry";
@@ -69,7 +70,7 @@ const RevealLink: Component<LinkPayload> = (p) => {
return (
<div class="flex items-center gap-2">
<span class="text-lg">📄</span>
<TbOutlineFileText class="text-lg" />
<div>
<a
href={target()}
+4 -2
View File
@@ -7,6 +7,7 @@
*/
import { z } from "zod";
import { TbOutlineArrowRight, TbOutlineDice5 } from "solid-icons/tb";
import { registerMessageType } from "../registry";
import { rollFormula } from "../../dice";
@@ -55,9 +56,10 @@ registerMessageType<RollPayload>({
return (
<div class="space-y-0.5">
<div class="flex items-center gap-1.5">
<span class="text-lg">🎲</span>
<TbOutlineDice5 class="text-lg" />
<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>
</div>
+2 -1
View File
@@ -10,6 +10,7 @@
*/
import { z } from "zod";
import { TbOutlineHash } from "solid-icons/tb";
import { registerMessageType } from "../registry";
import { journalSetState } from "../../stores/journalStream";
import { produce } from "solid-js/store";
@@ -35,7 +36,7 @@ registerMessageType<VarPayload>({
render: (p) => {
return (
<div class="flex items-center gap-1.5">
<span class="text-lg">🔢</span>
<TbOutlineHash class="text-lg" />
<span class="font-mono text-sm">
{p.key} = <span class="font-bold">{p.value}</span>
</span>
+3 -2
View File
@@ -1,4 +1,5 @@
import { For, Show, createEffect, createSignal, on } from "solid-js";
import { TbOutlineChevronLeft, TbOutlineChevronRight } from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore";
export interface CardListProps {
@@ -67,7 +68,7 @@ export function CardList(props: CardListProps) {
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"
>
‹
<TbOutlineChevronLeft />
</button>
<span class="text-xs text-gray-500 tabular-nums">
{page() + 1} / {totalPages()}
@@ -77,7 +78,7 @@ export function CardList(props: CardListProps) {
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"
>
›
<TbOutlineChevronRight />
</button>
</div>
</Show>
+17 -3
View File
@@ -1,5 +1,11 @@
import { Show, createSignal, onMount } from "solid-js";
import { Portal } from "solid-js/web";
import {
TbOutlineCheck,
TbOutlineCopy,
TbOutlineDownload,
TbOutlinePencil,
} from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore";
export interface DeckHeaderProps {
@@ -43,7 +49,15 @@ export function DeckHeader(props: DeckHeaderProps) {
: "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>
{/* 导出 PDF 按钮 */}
@@ -51,7 +65,7 @@ export function DeckHeader(props: DeckHeaderProps) {
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"
>
📥 导出 PDF
<TbOutlineDownload class="inline-block align-[-0.125em]" /> 导出 PDF
</button>
{/* 复制代码按钮 */}
@@ -59,7 +73,7 @@ export function DeckHeader(props: DeckHeaderProps) {
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"
>
📋 复制代码
<TbOutlineCopy class="inline-block align-[-0.125em]" /> 复制代码
</button>
</div>
+2 -1
View File
@@ -1,5 +1,6 @@
import { createSignal, For, Show, createMemo } from "solid-js";
import { Portal } from "solid-js/web";
import { TbOutlineDownload } from "solid-icons/tb";
import { parsePlt, extractCutPaths } from "../../plotcutter/parser";
import { generateTravelPaths, travelPathsToSvg } from "../../plotcutter/layout";
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"
disabled={parsedData().cardPaths.length === 0}
>
📥 下载 PLT
<TbOutlineDownload /> 下载 PLT
</button>
<button
@@ -1,4 +1,5 @@
import { Show } from 'solid-js';
import { TbOutlineCircleX, TbOutlineX } from 'solid-icons/tb';
import type { DeckStore } from './hooks/deckStore';
export interface PrintPreviewFooterProps {
@@ -26,14 +27,14 @@ export function PrintPreviewFooter(props: PrintPreviewFooterProps) {
<Show when={store.state.exportError}>
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2 text-red-600">
<span>❌</span>
<TbOutlineCircleX />
<span class="text-sm font-medium">{store.state.exportError}</span>
</div>
<button
onClick={() => store.actions.clearExportError()}
class="text-gray-500 hover:text-gray-700 cursor-pointer"
>
✕
<TbOutlineX />
</button>
</div>
</Show>
@@ -1,4 +1,9 @@
import { createSignal, For, onCleanup, onMount, Show } from "solid-js";
import {
TbOutlineDownload,
TbOutlinePlus,
TbOutlineRuler,
} from "solid-icons/tb";
import type { DeckStore } from "./hooks/deckStore";
import { getCompatibleDecks } from "./hooks/deck-registry";
@@ -77,7 +82,7 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {
: "添加其他牌组到打印"
}
>
<span>➕</span>
<TbOutlinePlus />
<span>添加牌组</span>
<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">
@@ -232,14 +237,14 @@ export function PrintPreviewHeader(props: PrintPreviewHeaderProps) {
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"
>
<span>📐</span>
<TbOutlineRuler />
<span>PLT 预览</span>
</button>
<button
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"
>
<span>📥</span>
<TbOutlineDownload />
<span>导出 PDF</span>
</button>
<button
@@ -1,10 +1,27 @@
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 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 { 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 {
store: DeckStore;
@@ -22,65 +39,67 @@ export interface LayerRowProps {
onRemove: () => void;
}
const ORIENTATIONS = [
{ value: "n" as const, label: "↑ 北" },
{ value: "e" as const, label: "→ 东" },
{ value: "s" as const, label: "↓ 南" },
{ value: "w" as const, label: "← 西" },
const ORIENTATIONS: {
value: "n" | "e" | "s" | "w";
label: string;
icon: IconTypes;
}[] = [
{ 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 }[] = [
{ value: "", icon: alignCenterIcon },
{ value: "l", icon: alignLeftIcon },
{ value: "c", icon: alignCenterIcon },
{ value: "r", icon: alignRightIcon },
{ value: "tl", icon: "↖" },
{ value: "tc", icon: "↑" },
{ value: "tr", icon: "↗" },
{ value: "bl", icon: "↙" },
{ value: "bc", icon: "↓" },
{ value: "br", icon: "↘" },
const ALIGNS: { value: Align | ""; icon: IconTypes }[] = [
{ value: "", icon: TbOutlineAlignCenter },
{ value: "l", icon: TbOutlineAlignLeft },
{ value: "c", icon: TbOutlineAlignCenter },
{ value: "r", icon: TbOutlineAlignRight },
{ value: "tl", icon: TbOutlineArrowUpLeft },
{ value: "tc", icon: TbOutlineArrowUp },
{ value: "tr", icon: TbOutlineArrowUpRight },
{ value: "bl", icon: TbOutlineArrowDownLeft },
{ value: "bc", icon: TbOutlineArrowDown },
{ value: "br", icon: TbOutlineArrowDownRight },
];
const FONT_PRESETS = [3, 5, 8, 12] as const;
function orientChar(v: string) {
function orientIcon(v: string): IconTypes {
switch (v) {
case "n":
return "↑";
return TbOutlineArrowUp;
case "e":
return "→";
return TbOutlineArrowRight;
case "s":
return "↓";
return TbOutlineArrowDown;
case "w":
return "←";
return TbOutlineArrowLeft;
default:
return "↑";
return TbOutlineArrowUp;
}
}
function alignIcon(v: string): import("solid-js").JSX.Element {
function alignIcon(v: string): IconTypes {
switch (v) {
case "tl":
return "↖";
return TbOutlineArrowUpLeft;
case "tc":
return "↑";
return TbOutlineArrowUp;
case "tr":
return "↗";
return TbOutlineArrowUpRight;
case "bl":
return "↙";
return TbOutlineArrowDownLeft;
case "bc":
return "↓";
return TbOutlineArrowDown;
case "br":
return "↘";
return TbOutlineArrowDownRight;
case "l":
return <img src={alignLeftIcon} alt="align" class="w-5 h-5 not-prose" />;
return TbOutlineAlignLeft;
case "r":
return <img src={alignRightIcon} alt="align" class="w-5 h-5 not-prose" />;
return TbOutlineAlignRight;
default:
return (
<img src={alignCenterIcon} alt="align" class="w-5 h-5 not-prose" />
);
return TbOutlineAlignCenter;
}
}
@@ -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"
title="拖动排序"
>
⠿
<TbOutlineGripVertical class="inline-block align-[-0.125em]" />
</button>
<button
@@ -133,7 +152,11 @@ export function LayerRow(props: LayerRowProps) {
class="shrink-0 text-sm cursor-pointer select-none"
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>
<span
@@ -144,7 +167,7 @@ export function LayerRow(props: LayerRowProps) {
</span>
<DropdownButton
icon={orientChar(props.layer.orientation || "n")}
icon={orientIcon(props.layer.orientation || "n")}
visible={props.layer.visible}
open={props.openDropdown === `orient-${props.index}`}
onToggle={() =>
@@ -162,6 +185,10 @@ export function LayerRow(props: LayerRowProps) {
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"
>
<Dynamic
component={o.icon}
class="inline-block align-[-0.125em]"
/>{" "}
{o.label}
</button>
)}
@@ -187,13 +214,11 @@ export function LayerRow(props: LayerRowProps) {
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"
>
{o.icon.endsWith(".png") ? (
<img src={o.icon} alt="" class="w-4 h-4 not-prose max-w-none" />
) : (
<span class="w-4 h-4 flex items-center justify-center not-prose">
{o.icon}
</span>
)}
<Dynamic
component={o.icon}
size={16}
class="not-prose max-w-none"
/>
</button>
)}
</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"}`}
title="删除图层"
>
✕
<TbOutlineTrash size={14} />
</button>
</div>
);
}
function DropdownButton(props: {
icon: import("solid-js").JSX.Element;
icon: IconTypes;
visible: boolean;
open: boolean;
onToggle: () => void;
title: string;
children: import("solid-js").JSX.Element;
children: JSX.Element;
}) {
return (
<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" : ""}`}
title={props.title}
>
{props.icon}
<Dynamic component={props.icon} size={16} />
</button>
{props.open && (
<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 { createSignal, onMount } from "solid-js";
import { TbOutlineDice5 } from "solid-icons/tb";
import { rollFormula } from "./dice";
export interface DiceProps {
@@ -86,7 +87,7 @@ customElement("md-dice", { key: "" }, (props, { element }) => {
class="text-blue-600 hover:text-blue-800 cursor-pointer"
title={rollDetail() || "掷骰子"}
>
🎲
<TbOutlineDice5 class="inline-block align-[-0.125em]" />
</span>
<a
onClick={handleRoll}
+2 -1
View File
@@ -1,5 +1,6 @@
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";
@@ -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"
title="复制所有 pin 坐标"
>
📋 复制
<TbOutlineCopy /> 复制
</button>
</div>
</Show>
+2 -1
View File
@@ -7,6 +7,7 @@ import {
createMemo,
createResource,
} from "solid-js";
import { TbOutlineDice5 } from "solid-icons/tb";
import { parseMarkdown } from "../markdown";
import { parseCSVString, CSV, processVariables } from "./utils/csv-loader";
import { resolveContentRef } from "./utils/resolve-content";
@@ -210,7 +211,7 @@ customElement(
class="text-gray-500 hover:text-gray-700 shrink-0 cursor-pointer"
title="随机切换"
>
🎲
<TbOutlineDice5 class="inline-block align-[-0.125em]" />
</button>
</Show>
<div
+8 -3
View File
@@ -1,5 +1,10 @@
import { customElement, noShadowDOM } from 'solid-element';
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 { 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"
title="重新开始"
>
🔄
<TbOutlineRotate class="inline-block align-[-0.125em]" />
</button>
<button
onClick={() => advance()}
@@ -90,7 +95,7 @@ customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { el
transition-colors cursor-pointer"
title="继续"
>
⏩
<TbOutlinePlayerTrackNext class="inline-block align-[-0.125em]" />
</button>
</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
rounded border border-blue-200 transition-colors cursor-pointer"
>
→ {option.text}
<TbOutlineArrowRight class="inline-block align-[-0.125em]" /> {option.text}
</button>
)}
</For>