refactor(components): remove md-commander widget
The journal now owns command history and tracking, so retire the md-commander widget. Its dice engine was extracted to src/components/dice in the previous commit. Removes the custom element, tracker/command stores, completions, useCommander, Emmet parser, the doc entry, test fixtures, and the known-element catalog entry (reference docs regenerated). BREAKING CHANGE: :md-commander is no longer a known element; content using it now fails tttk check with TTTK-D001.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: tttk-authoring
|
name: tttk-authoring
|
||||||
description: Author and edit TTRPG game design documents for the tttk (Tabletop Toolkit) CLI. Use when writing or modifying markdown content that uses tttk custom elements (:md-dice, :md-table, :md-deck, :md-pins, :md-commander, etc.), custom fenced code blocks (role=spark-table / declare / file / tag), companion CSV data files, or when previewing content with `tttk serve` and validating it with `tttk check`.
|
description: Author and edit TTRPG game design documents for the tttk (Tabletop Toolkit) CLI. Use when writing or modifying markdown content that uses tttk custom elements (:md-dice, :md-table, :md-deck, :md-pins, etc.), custom fenced code blocks (role=spark-table / declare / file / tag), companion CSV data files, or when previewing content with `tttk serve` and validating it with `tttk check`.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Authoring tttk game design documents
|
# Authoring tttk game design documents
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ Nest with more colons (`::::`, `:::::`).
|
|||||||
| `:md-border` | color or border image | `width`, `slice` |
|
| `:md-border` | color or border image | `width`, `slice` |
|
||||||
| `:md-font` | font family | `source`, `weight`, `color` |
|
| `:md-font` | font family | `source`, `weight`, `color` |
|
||||||
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
|
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
|
||||||
| `:md-commander` | command template CSV | `placeholder`, `class`, `height` |
|
|
||||||
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
|
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
|
||||||
|
|
||||||
### `:md-dice`
|
### `:md-dice`
|
||||||
@@ -205,19 +204,6 @@ Numbered markers on an image.
|
|||||||
:md-pins[./images/dungeon.png]{labelStart="1"}
|
:md-pins[./images/dungeon.png]{labelStart="1"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `:md-commander`
|
|
||||||
|
|
||||||
Interactive command history / tracker panel.
|
|
||||||
|
|
||||||
The body is a CSV of command templates (`command,parameter,label,description,insertedText`).
|
|
||||||
|
|
||||||
- Body: command template CSV
|
|
||||||
- Attributes: `placeholder`, `class`, `height`
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
:md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `:md-yarn-spinner`
|
### `:md-yarn-spinner`
|
||||||
|
|
||||||
Runs a Yarn Spinner dialogue file.
|
Runs a Yarn Spinner dialogue file.
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ npx tttk check ./content
|
|||||||
| 📊 表格 | `:md-table[./data.csv]` | 标签页式表格 |
|
| 📊 表格 | `:md-table[./data.csv]` | 标签页式表格 |
|
||||||
| 🃏 卡牌 | `:md-deck[./cards.csv]` | 卡牌布局(推荐 `yaml role=tag` 写法) |
|
| 🃏 卡牌 | `:md-deck[./cards.csv]` | 卡牌布局(推荐 `yaml role=tag` 写法) |
|
||||||
| 📍 标记 | `:md-pins[./map.png]{pins="A:25,50"}` | 图片标记 |
|
| 📍 标记 | `:md-pins[./map.png]{pins="A:25,50"}` | 图片标记 |
|
||||||
| 📋 追踪器 | `:md-commander[./commands.csv]` | 命令历史和状态追踪 |
|
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Commander Test
|
|
||||||
|
|
||||||
:md-commander[./commands.csv]
|
|
||||||
|
|
||||||
## Lonelog
|
|
||||||
|
|
||||||
`我` 走进酒吧。
|
|
||||||
|
|
||||||
`oracle` 酒吧里人多吗?
|
|
||||||
|
|
||||||
`roll` d20 = 18 `prompt` 是的
|
|
||||||
|
|
||||||
`fact` 酒吧里有很多人。
|
|
||||||
|
|
||||||
:[blah]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
command,parameter,label,description,insertedText
|
|
||||||
roll,emmet,2d6,2D6,2d6
|
|
||||||
roll,emmet,d20,D20,d20
|
|
||||||
track,emmet,monk,破戒佛爷,"破戒佛爷.bandit.monk[gd=4/4,str=14/14,cla=10,spi=8]"
|
|
||||||
track,emmet,bandit,龙虎寨山贼,"龙虎寨山贼.bandit[gd=2/2,str=10/10,cla=10,spi=10]"
|
|
||||||
track,emmet,khitan,契丹护卫,"契丹护卫.khitan.guard[gd=4/4,ap=2,str=14/14,cla=14,spi=14]"
|
|
||||||
|
@@ -46,7 +46,6 @@ tttk/
|
|||||||
│ │ ├── md-table.tsx # 表格组件
|
│ │ ├── md-table.tsx # 表格组件
|
||||||
│ │ ├── md-deck/ # 卡牌组件
|
│ │ ├── md-deck/ # 卡牌组件
|
||||||
│ │ ├── md-pins.tsx # 标记组件
|
│ │ ├── md-pins.tsx # 标记组件
|
||||||
│ │ ├── md-commander/ # 命令追踪器
|
|
||||||
│ │ ├── md-yarn-spinner.tsx # 叙事线组件
|
│ │ ├── md-yarn-spinner.tsx # 叙事线组件
|
||||||
|
|
||||||
│ │ ├── Article.tsx # 文章组件
|
│ │ ├── Article.tsx # 文章组件
|
||||||
|
|||||||
@@ -461,36 +461,6 @@ data-config:
|
|||||||
:md-yarn-spinner[./story.yarn]
|
:md-yarn-spinner[./story.yarn]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 📋 命令追踪器 (md-commander)
|
|
||||||
|
|
||||||
支持命令历史和状态追踪。
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
:md-commander
|
|
||||||
```
|
|
||||||
|
|
||||||
**追踪器命令:**
|
|
||||||
|
|
||||||
```
|
|
||||||
track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Emmet 简写语法:**
|
|
||||||
|
|
||||||
- `#id` - 设置 ID
|
|
||||||
- `.class` - 添加类别
|
|
||||||
- `[attr=value]` - 设置属性
|
|
||||||
|
|
||||||
**属性类型:**
|
|
||||||
|
|
||||||
| 类型 | 格式 | 显示 |
|
|
||||||
|------|------|------|
|
|
||||||
| progress | `x/y` | 进度条 |
|
|
||||||
| count | 整数 | 数字计数器 |
|
|
||||||
| string | 文本 | 文本字段 |
|
|
||||||
|
|
||||||
## YAML 标签
|
## YAML 标签
|
||||||
|
|
||||||
使用 ```yaml role=tag 代码块创建自定义标签(`yaml` 语言可被语法高亮):
|
使用 ```yaml role=tag 代码块创建自定义标签(`yaml` 语言可被语法高亮):
|
||||||
|
|||||||
@@ -335,8 +335,6 @@ MCP 服务器提供以下 Resources,包含 TTRPG Tools 的文档和参考材
|
|||||||
- `:md-table` - 表格组件
|
- `:md-table` - 表格组件
|
||||||
- `:md-deck` - 卡牌组件
|
- `:md-deck` - 卡牌组件
|
||||||
- `:md-yarn-spinner` - 叙事线组件
|
- `:md-yarn-spinner` - 叙事线组件
|
||||||
|
|
||||||
- `:md-commander` - 命令追踪器
|
|
||||||
- YAML 标签
|
- YAML 标签
|
||||||
- Mermaid 图表
|
- Mermaid 图表
|
||||||
- 警告/提示块
|
- 警告/提示块
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ Nest with more colons (`::::`, `:::::`).
|
|||||||
| `:md-border` | color or border image | `width`, `slice` |
|
| `:md-border` | color or border image | `width`, `slice` |
|
||||||
| `:md-font` | font family | `source`, `weight`, `color` |
|
| `:md-font` | font family | `source`, `weight`, `color` |
|
||||||
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
|
| `:md-pins` | image path | `pins`, `fixed`, `labelStart` |
|
||||||
| `:md-commander` | command template CSV | `placeholder`, `class`, `height` |
|
|
||||||
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
|
| `:md-yarn-spinner` | Yarn Spinner file | `start` |
|
||||||
|
|
||||||
### `:md-dice`
|
### `:md-dice`
|
||||||
@@ -205,19 +204,6 @@ Numbered markers on an image.
|
|||||||
:md-pins[./images/dungeon.png]{labelStart="1"}
|
:md-pins[./images/dungeon.png]{labelStart="1"}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `:md-commander`
|
|
||||||
|
|
||||||
Interactive command history / tracker panel.
|
|
||||||
|
|
||||||
The body is a CSV of command templates (`command,parameter,label,description,insertedText`).
|
|
||||||
|
|
||||||
- Body: command template CSV
|
|
||||||
- Attributes: `placeholder`, `class`, `height`
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
:md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `:md-yarn-spinner`
|
### `:md-yarn-spinner`
|
||||||
|
|
||||||
Runs a Yarn Spinner dialogue file.
|
Runs a Yarn Spinner dialogue file.
|
||||||
|
|||||||
@@ -131,16 +131,6 @@ export const KNOWN_ELEMENTS: readonly ElementSpec[] = [
|
|||||||
':md-pins[./images/dungeon.png]{labelStart="1"}',
|
':md-pins[./images/dungeon.png]{labelStart="1"}',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "md-commander",
|
|
||||||
body: "command template CSV",
|
|
||||||
attrs: ["placeholder", "class", "height"],
|
|
||||||
summary: "Interactive command history / tracker panel.",
|
|
||||||
details:
|
|
||||||
"The body is a CSV of command templates (`command,parameter,label,description,insertedText`).",
|
|
||||||
refKind: "csv",
|
|
||||||
examples: [':md-commander[./commands.csv]{placeholder="输入命令…" height="400px"}'],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "md-yarn-spinner",
|
name: "md-yarn-spinner",
|
||||||
body: "Yarn Spinner file",
|
body: "Yarn Spinner file",
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ import mdEmbedRaw from "../doc-entries/md-embed.md";
|
|||||||
import mdDeckRaw from "../doc-entries/md-deck.md";
|
import mdDeckRaw from "../doc-entries/md-deck.md";
|
||||||
import mdYarnRaw from "../doc-entries/md-yarn-spinner.md";
|
import mdYarnRaw from "../doc-entries/md-yarn-spinner.md";
|
||||||
|
|
||||||
import mdCommanderRaw from "../doc-entries/md-commander.md";
|
|
||||||
|
|
||||||
const rawDocuments: string[] = [
|
const rawDocuments: string[] = [
|
||||||
mdDiceRaw,
|
mdDiceRaw,
|
||||||
mdTableRaw,
|
mdTableRaw,
|
||||||
@@ -63,8 +61,6 @@ const rawDocuments: string[] = [
|
|||||||
mdEmbedRaw,
|
mdEmbedRaw,
|
||||||
mdDeckRaw,
|
mdDeckRaw,
|
||||||
mdYarnRaw,
|
mdYarnRaw,
|
||||||
|
|
||||||
mdCommanderRaw,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let _entries: DocEntry[] | null = null;
|
let _entries: DocEntry[] | null = null;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import "./md-border";
|
|||||||
import "./md-font";
|
import "./md-font";
|
||||||
import "./md-embed";
|
import "./md-embed";
|
||||||
import "./md-deck";
|
import "./md-deck";
|
||||||
import "./md-commander/index";
|
|
||||||
import "./md-yarn-spinner";
|
import "./md-yarn-spinner";
|
||||||
|
|
||||||
// 导出组件
|
// 导出组件
|
||||||
@@ -33,29 +32,3 @@ export type { FontProps } from "./md-font";
|
|||||||
export type { BorderProps } from "./md-border";
|
export type { BorderProps } from "./md-border";
|
||||||
export type { EmbedProps } from "./md-embed";
|
export type { EmbedProps } from "./md-embed";
|
||||||
export type { YarnSpinnerProps } from "./md-yarn-spinner";
|
export type { YarnSpinnerProps } from "./md-yarn-spinner";
|
||||||
|
|
||||||
// 导出 md-commander 相关
|
|
||||||
export type {
|
|
||||||
MdCommanderProps,
|
|
||||||
MdCommanderCommand,
|
|
||||||
MdCommanderCommandMap,
|
|
||||||
MdCommanderParameter,
|
|
||||||
MdCommanderOption,
|
|
||||||
MdCommanderOptionType,
|
|
||||||
CommanderEntry,
|
|
||||||
CompletionItem,
|
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerAttributeType,
|
|
||||||
TrackerCommand,
|
|
||||||
TrackerViewMode,
|
|
||||||
} from "./md-commander/types";
|
|
||||||
export { TabBar } from "./md-commander/TabBar";
|
|
||||||
export { TrackerView } from "./md-commander/TrackerView";
|
|
||||||
export { CommanderEntries } from "./md-commander/CommanderEntries";
|
|
||||||
export { CommanderInput } from "./md-commander/CommanderInput";
|
|
||||||
export { useCommander } from "./md-commander/hooks";
|
|
||||||
export {
|
|
||||||
initializeCommands,
|
|
||||||
getCommands,
|
|
||||||
} from "./md-commander/stores/commandsStore";
|
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
import { type Component, Show, For, createSignal, createEffect, onCleanup } from "solid-js";
|
|
||||||
import type { TrackerAttribute } from "./types";
|
|
||||||
|
|
||||||
export interface AttributeTooltipProps {
|
|
||||||
position: { x: number; y: number };
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
classes: string[];
|
|
||||||
onUpdate: (attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
onClassesChange: (classes: string[]) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AttributeTooltip: Component<AttributeTooltipProps> = (props) => {
|
|
||||||
const [position, setPosition] = createSignal(props.position);
|
|
||||||
const [newClass, setNewClass] = createSignal("");
|
|
||||||
|
|
||||||
// 点击外部关闭
|
|
||||||
createEffect(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.closest('body') && !target.closest(".attribute-tooltip")) {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
onCleanup(() => document.removeEventListener("click", handleClickOutside));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理属性更新
|
|
||||||
const handleUpdate = (attrName: string, value: any) => {
|
|
||||||
const attr = props.attributes[attrName];
|
|
||||||
props.onUpdate(attrName, { ...attr, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理滚轮增减(progress 和 count 类型)
|
|
||||||
const handleWheel = (e: WheelEvent, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (attr.type === "count") {
|
|
||||||
const delta = e.deltaY > 0 ? -1 : 1;
|
|
||||||
const newValue = Math.max(0, (attr.value as number) + delta);
|
|
||||||
handleUpdate(attrName, newValue);
|
|
||||||
} else if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
const delta = e.deltaY > 0 ? -1 : 1;
|
|
||||||
// 滚轮向上/下调整当前值 x
|
|
||||||
const newX = Math.max(0, Math.min(val.y, val.x + delta));
|
|
||||||
handleUpdate(attrName, { x: newX, y: val.y });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加 class
|
|
||||||
const handleAddClass = () => {
|
|
||||||
const className = newClass().trim();
|
|
||||||
if (className && !props.classes.includes(className)) {
|
|
||||||
props.onClassesChange([...props.classes, className]);
|
|
||||||
setNewClass("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除 class
|
|
||||||
const handleRemoveClass = (e: MouseEvent, className: string) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
props.onClassesChange(props.classes.filter((c) => c !== className));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 渲染不同类型的属性编辑器
|
|
||||||
const renderEditor = (attrName: string, attr: TrackerAttribute) => {
|
|
||||||
if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
const percentage = val.y > 0 ? (val.x / val.y) * 100 : 0;
|
|
||||||
return (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
{/* 进度条 */}
|
|
||||||
<div
|
|
||||||
class="w-full h-4 bg-gray-200 rounded-full overflow-hidden cursor-pointer"
|
|
||||||
title="滚动鼠标滚轮增减"
|
|
||||||
onWheel={(e) => handleWheel(e, attrName, attr)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-200"
|
|
||||||
style={{ width: `${Math.min(100, percentage)}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* 输入框 */}
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={val.x}
|
|
||||||
onChange={(e) => handleUpdate(attrName, { x: parseInt(e.target.value) || 0, y: val.y })}
|
|
||||||
onWheel={(e) => handleWheel(e, attrName, attr)}
|
|
||||||
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<span class="text-gray-500">/</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={val.y}
|
|
||||||
onChange={(e) => handleUpdate(attrName, { x: val.x, y: parseInt(e.target.value) || 0 })}
|
|
||||||
class="w-14 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr.type === "count") {
|
|
||||||
const value = attr.value as number;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
onWheel={(e) => handleWheel(e, attrName, attr)}
|
|
||||||
title="滚动鼠标滚轮增减"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
|
|
||||||
onClick={() => handleUpdate(attrName, value - 1)}
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => handleUpdate(attrName, parseInt(e.target.value) || 0)}
|
|
||||||
onWheel={(e) => handleWheel(e, attrName, attr)}
|
|
||||||
class="w-12 px-1 py-0.5 border border-gray-300 rounded text-center text-sm"
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="w-7 h-7 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded text-lg font-bold transition-colors"
|
|
||||||
onClick={() => handleUpdate(attrName, value + 1)}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attr.type === "string") {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={attr.value as string}
|
|
||||||
onChange={(e) => handleUpdate(attrName, e.target.value)}
|
|
||||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span class="text-gray-500">未知类型</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="attribute-tooltip fixed z-50 bg-white border border-gray-300 rounded-lg shadow-xl p-3 min-w-[150px]"
|
|
||||||
style={{
|
|
||||||
left: `${position().x}px`,
|
|
||||||
top: `${position().y}px`,
|
|
||||||
}}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-2 pb-2 border-b border-gray-200">
|
|
||||||
<span class="font-bold text-gray-700 text-sm">编辑属性</span>
|
|
||||||
<button
|
|
||||||
class="text-gray-400 hover:text-gray-600 text-lg leading-none"
|
|
||||||
onClick={props.onClose}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
{/* Class 列表编辑器 */}
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 font-medium">Classes</label>
|
|
||||||
<div class="flex flex-wrap gap-1 mb-1">
|
|
||||||
<For each={props.classes}>
|
|
||||||
{(className) => (
|
|
||||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">
|
|
||||||
.{className}
|
|
||||||
<button
|
|
||||||
class="text-blue-400 hover:text-blue-600 font-bold leading-none"
|
|
||||||
onClick={(e) => handleRemoveClass(e, className)}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newClass()}
|
|
||||||
onInput={(e) => setNewClass((e.target as HTMLInputElement).value)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleAddClass();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder="添加 class..."
|
|
||||||
class="flex-1 px-2 py-1 border border-gray-300 rounded text-sm"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="px-2 py-1 bg-gray-200 hover:bg-gray-300 rounded text-sm font-medium transition-colors"
|
|
||||||
onClick={handleAddClass}
|
|
||||||
>
|
|
||||||
添加
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 属性编辑器 */}
|
|
||||||
<For each={Object.entries(props.attributes)}>
|
|
||||||
{([name, attr]) => (
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<label class="text-xs text-gray-500 font-medium">{name}</label>
|
|
||||||
{renderEditor(name, attr)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { type Component, For, Show } from "solid-js";
|
|
||||||
import type { CommanderEntry } from "./types";
|
|
||||||
import { getResultClass } from "./hooks";
|
|
||||||
|
|
||||||
export interface CommanderEntriesProps {
|
|
||||||
entries: () => CommanderEntry[];
|
|
||||||
onCommandClick?: (command: string) => void;
|
|
||||||
loading?: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommanderEntries: Component<CommanderEntriesProps> = (props) => {
|
|
||||||
let containerRef: HTMLDivElement | undefined;
|
|
||||||
|
|
||||||
const handleCommandClick = (command: string) => {
|
|
||||||
if (props.onCommandClick) {
|
|
||||||
props.onCommandClick(command);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
class="commander-entries flex-1 overflow-auto p-3 bg-white space-y-2"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={props.loading}
|
|
||||||
fallback={
|
|
||||||
<Show
|
|
||||||
when={props.error}
|
|
||||||
fallback={
|
|
||||||
<Show
|
|
||||||
when={props.entries().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="text-gray-400 text-center py-8">暂无命令执行记录</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={props.entries()}>
|
|
||||||
{(entry) => (
|
|
||||||
<div class="border-l-2 border-gray-300 pl-3 py-1">
|
|
||||||
<div class="flex items-center justify-between text-xs text-gray-500 mb-1">
|
|
||||||
<span
|
|
||||||
class="font-mono cursor-pointer hover:text-blue-600 hover:underline truncate max-w-64"
|
|
||||||
onClick={() => handleCommandClick(entry.command)}
|
|
||||||
title="点击复制到输入框"
|
|
||||||
>
|
|
||||||
{entry.command}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{entry.timestamp.toLocaleTimeString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class={`text-sm whitespace-pre-wrap ${getResultClass(entry.result.type)}`}
|
|
||||||
innerHTML={entry.result.isHtml ? entry.result.message : undefined}
|
|
||||||
>
|
|
||||||
{!entry.result.isHtml && entry.result.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="text-red-500 text-center py-8">{props.error}</div>
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-center h-full">
|
|
||||||
<div class="text-gray-500 flex items-center gap-2">
|
|
||||||
<div class="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
|
||||||
<span>加载命令模板中...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import {type Component, For, Show, createEffect, on} from "solid-js";
|
|
||||||
import type { CompletionItem } from "./types";
|
|
||||||
|
|
||||||
export interface CommanderInputProps {
|
|
||||||
placeholder?: string;
|
|
||||||
inputValue: () => string;
|
|
||||||
onInput: (e: Event) => void;
|
|
||||||
onKeyDown: (e: KeyboardEvent) => void;
|
|
||||||
onFocus: () => void;
|
|
||||||
onBlur: () => void;
|
|
||||||
onSubmit: () => void;
|
|
||||||
showCompletions: () => boolean;
|
|
||||||
completions: () => CompletionItem[];
|
|
||||||
selectedCompletion: () => number;
|
|
||||||
onSelectCompletion: (idx: number) => void;
|
|
||||||
onAcceptCompletion: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommanderInput: Component<CommanderInputProps> = (props) => {
|
|
||||||
let containerRef: HTMLDivElement | undefined;
|
|
||||||
|
|
||||||
// 当选中项变化时自动滚动到可见区域
|
|
||||||
createEffect(
|
|
||||||
on(
|
|
||||||
() => props.selectedCompletion(),
|
|
||||||
() => {
|
|
||||||
if (containerRef) {
|
|
||||||
const selectedItem = containerRef.querySelector('[data-selected="true"]');
|
|
||||||
if (selectedItem) {
|
|
||||||
selectedItem.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const maxVisible = 3;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex items-center border-b border-gray-300 bg-gray-50 px-3 py-2">
|
|
||||||
<span class="text-gray-500 mr-2">❯</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={props.inputValue()}
|
|
||||||
onInput={props.onInput}
|
|
||||||
onKeyDown={props.onKeyDown}
|
|
||||||
onFocus={props.onFocus}
|
|
||||||
onBlur={props.onBlur}
|
|
||||||
placeholder={props.placeholder || "输入命令..."}
|
|
||||||
class="flex-1 bg-transparent outline-none text-gray-800"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={props.onSubmit}
|
|
||||||
class="ml-2 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
|
||||||
>
|
|
||||||
执行
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 自动补全下拉框 - 向上弹出 */}
|
|
||||||
<Show when={props.showCompletions() && props.completions().length > 0}>
|
|
||||||
<div
|
|
||||||
ref={containerRef}
|
|
||||||
class="absolute left-0 bottom-full w-full bg-white border border-gray-300 shadow-lg mb-1 overflow-auto"
|
|
||||||
style={{ "max-height": `${maxVisible * 40}px` }}
|
|
||||||
>
|
|
||||||
<For each={props.completions()}>{(comp, idx) => (
|
|
||||||
<div
|
|
||||||
data-selected={idx() === props.selectedCompletion()}
|
|
||||||
class={`px-3 py-2 cursor-pointer flex justify-between items-center ${
|
|
||||||
idx() === props.selectedCompletion()
|
|
||||||
? "bg-blue-100"
|
|
||||||
: "hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
props.onSelectCompletion(idx());
|
|
||||||
props.onAcceptCompletion();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
class={`text-xs px-1 rounded ${
|
|
||||||
comp.type === "command"
|
|
||||||
? "bg-purple-100 text-purple-700"
|
|
||||||
: comp.type === "option"
|
|
||||||
? "bg-green-100 text-green-700"
|
|
||||||
: "bg-gray-100 text-gray-700"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{comp.type}
|
|
||||||
</span>
|
|
||||||
<span class="font-mono text-sm">{comp.label}</span>
|
|
||||||
</div>
|
|
||||||
<Show when={comp.description}>
|
|
||||||
<span class="text-xs text-gray-500">{comp.description}</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { type Component } from "solid-js";
|
|
||||||
import type { TrackerViewMode } from "./types";
|
|
||||||
|
|
||||||
export interface TabBarProps {
|
|
||||||
mode: () => TrackerViewMode;
|
|
||||||
onModeChange: (mode: TrackerViewMode) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TabBar: Component<TabBarProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<div class="flex lg:hidden border-b border-gray-300 bg-gray-50">
|
|
||||||
<button
|
|
||||||
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
props.mode() === "history"
|
|
||||||
? "bg-white text-blue-600 border-b-2 border-blue-600"
|
|
||||||
: "text-gray-600 hover:text-gray-800 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => props.onModeChange("history")}
|
|
||||||
>
|
|
||||||
📜 历史
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
|
||||||
props.mode() === "tracker"
|
|
||||||
? "bg-white text-blue-600 border-b-2 border-blue-600"
|
|
||||||
: "text-gray-600 hover:text-gray-800 hover:bg-gray-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => props.onModeChange("tracker")}
|
|
||||||
>
|
|
||||||
📋 追踪
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { type Component, For, Show, createSignal } from "solid-js";
|
|
||||||
import type { TrackerItem, TrackerAttribute } from "./types";
|
|
||||||
import { AttributeTooltip } from "./AttributeTooltip";
|
|
||||||
|
|
||||||
export interface TrackerViewProps {
|
|
||||||
items: () => TrackerItem[];
|
|
||||||
onEditAttribute?: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
onClassesChange?: (index: number, classes: string[]) => void;
|
|
||||||
onRemoveClass?: (index: number, className: string) => void;
|
|
||||||
onMoveUp?: (index: number) => void;
|
|
||||||
onMoveDown?: (index: number) => void;
|
|
||||||
onRemove?: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TrackerView: Component<TrackerViewProps> = (props) => {
|
|
||||||
const [editingItem, setEditingItem] = createSignal<{
|
|
||||||
index: number;
|
|
||||||
position: { x: number; y: number };
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const formatAttributeValue = (attr: TrackerAttribute): string => {
|
|
||||||
if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
return `${val.x}/${val.y}`;
|
|
||||||
}
|
|
||||||
if (attr.type === "count") {
|
|
||||||
return String(attr.value);
|
|
||||||
}
|
|
||||||
return String(attr.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatAttributeShort = (name: string, attr: TrackerAttribute): string => {
|
|
||||||
const val = formatAttributeValue(attr);
|
|
||||||
return `${name}=${val}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理滚轮增减(progress 和 count 类型)
|
|
||||||
const handleWheel = (e: WheelEvent, index: number, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (attr.type === "count") {
|
|
||||||
const delta = e.deltaY > 0 ? -1 : 1;
|
|
||||||
const newValue = Math.max(0, (attr.value as number) + delta);
|
|
||||||
props.onEditAttribute?.(index, attrName, { ...attr, value: newValue });
|
|
||||||
} else if (attr.type === "progress") {
|
|
||||||
const val = attr.value as { x: number; y: number };
|
|
||||||
const delta = e.deltaY > 0 ? -1 : 1;
|
|
||||||
const newX = Math.max(0, Math.min(val.y, val.x + delta));
|
|
||||||
props.onEditAttribute?.(index, attrName, { ...attr, value: { x: newX, y: val.y } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagClick = (e: MouseEvent, index: number) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
||||||
setEditingItem({
|
|
||||||
index,
|
|
||||||
position: {
|
|
||||||
x: rect.left,
|
|
||||||
y: rect.bottom + 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateAttribute = (attrName: string, attr: TrackerAttribute) => {
|
|
||||||
const data = editingItem();
|
|
||||||
if (data) {
|
|
||||||
props.onEditAttribute?.(data.index, attrName, attr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClassesChange = (classes: string[]) => {
|
|
||||||
const data = editingItem();
|
|
||||||
if (data) {
|
|
||||||
props.onClassesChange?.(data.index, classes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="tracker-view flex-1 overflow-auto p-3 bg-white">
|
|
||||||
<Show
|
|
||||||
when={props.items().length > 0}
|
|
||||||
fallback={
|
|
||||||
<div class="text-gray-400 text-center py-8">暂无追踪项目</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<For each={props.items()}>
|
|
||||||
{(item, index) => (
|
|
||||||
<div class="border border-gray-200 rounded-lg p-2 bg-gray-50">
|
|
||||||
{/* 第一行:tag#id.class + 操作按钮 */}
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex flex-wrap items-center gap-2 flex-1 min-w-0">
|
|
||||||
<button
|
|
||||||
class="font-bold text-gray-800 whitespace-nowrap hover:text-blue-600 hover:underline cursor-pointer transition-colors"
|
|
||||||
title="点击编辑属性"
|
|
||||||
onClick={(e) => handleTagClick(e, index())}
|
|
||||||
>
|
|
||||||
{item.tag}
|
|
||||||
</button>
|
|
||||||
<Show when={item.id}>
|
|
||||||
<span class="text-xs text-purple-600 font-mono whitespace-nowrap">#{item.id}</span>
|
|
||||||
</Show>
|
|
||||||
<Show when={item.classes.length > 0}>
|
|
||||||
<span class="text-xs text-blue-600 font-mono whitespace-nowrap">.{item.classes.join(".")}</span>
|
|
||||||
</Show>
|
|
||||||
{/* 属性简写显示 */}
|
|
||||||
<Show when={Object.keys(item.attributes).length > 0}>
|
|
||||||
<span class="text-xs text-gray-500 font-mono whitespace-nowrap truncate flex gap-2">
|
|
||||||
<For each={Object.entries(item.attributes)}>
|
|
||||||
{([name, attr]) => (
|
|
||||||
<span
|
|
||||||
class="inline-block cursor-pointer hover:text-blue-600 hover:underline"
|
|
||||||
title={`${name}: ${formatAttributeValue(attr)} - 滚动鼠标滚轮增减`}
|
|
||||||
onWheel={(e) => handleWheel(e, index(), name, attr)}
|
|
||||||
>
|
|
||||||
{formatAttributeShort(name, attr)}{" "}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 flex-shrink-0 ml-2">
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded text-xs"
|
|
||||||
title="上移"
|
|
||||||
onClick={() => props.onMoveUp?.(index())}
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded text-xs"
|
|
||||||
title="下移"
|
|
||||||
onClick={() => props.onMoveDown?.(index())}
|
|
||||||
>
|
|
||||||
↓
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded text-xs"
|
|
||||||
title="移除"
|
|
||||||
onClick={() => props.onRemove?.(index())}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* 属性编辑工具提示 */}
|
|
||||||
<Show when={editingItem()}>
|
|
||||||
<AttributeTooltip
|
|
||||||
position={editingItem()!.position}
|
|
||||||
attributes={props.items()[editingItem()!.index]?.attributes || {}}
|
|
||||||
classes={props.items()[editingItem()!.index]?.classes || []}
|
|
||||||
onUpdate={handleUpdateAttribute}
|
|
||||||
onClassesChange={handleClassesChange}
|
|
||||||
onClose={() => setEditingItem(null)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
|
|
||||||
export const clearCommand: MdCommanderCommand = {
|
|
||||||
command: "clear",
|
|
||||||
description: "清空命令历史",
|
|
||||||
handler: () => ({
|
|
||||||
message: "命令历史已清空",
|
|
||||||
type: "success",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
|
||||||
|
|
||||||
export function setupHelpCommand(commands: MdCommanderCommandMap): MdCommanderCommand {
|
|
||||||
return {
|
|
||||||
command: "help",
|
|
||||||
description: "显示帮助信息或特定命令的帮助",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "cmd",
|
|
||||||
description: "要查询的命令名",
|
|
||||||
type: "enum",
|
|
||||||
values: Object.keys(commands).filter((k) => k !== "help"),
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args, cmds) => {
|
|
||||||
const cmdName = args.params.cmd;
|
|
||||||
if (cmdName && cmds?.[cmdName]) {
|
|
||||||
return {
|
|
||||||
message: `命令:${cmdName}\n描述:${cmds[cmdName]?.description || "无描述"}`,
|
|
||||||
type: "info",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const cmdList = Object.keys(cmds || {}).filter((k) => k !== "help").join(", ");
|
|
||||||
return {
|
|
||||||
message: `可用命令:${cmdList}`,
|
|
||||||
type: "info",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export { setupHelpCommand } from "./help";
|
|
||||||
export { clearCommand } from "./clear";
|
|
||||||
export { rollCommand } from "./roll";
|
|
||||||
export { trackCommand, untrackCommand, listTrackCommand } from "./tracker";
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
import { rollFormula } from "../../dice";
|
|
||||||
|
|
||||||
export const rollCommand: MdCommanderCommand = {
|
|
||||||
command: "roll",
|
|
||||||
description: "掷骰子 - 支持骰池、修饰符和组合",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "formula",
|
|
||||||
description: "骰子公式,如 3d6+{d4,d8}kh2-5",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args) => {
|
|
||||||
const formula = args.params.formula || "1d6";
|
|
||||||
const result = rollFormula(formula);
|
|
||||||
return {
|
|
||||||
message: result.success ? result.result.detail : `错误:${result.error ?? formula}`,
|
|
||||||
isHtml: result.success,
|
|
||||||
type: result.success ? "success" : "error",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import type { MdCommanderCommand } from "../types";
|
|
||||||
import { parseEmmet } from "../utils";
|
|
||||||
import { addTrackerItem, removeTrackerItem, getTrackerItems, findTrackerItem } from "../stores";
|
|
||||||
|
|
||||||
export const trackCommand: MdCommanderCommand = {
|
|
||||||
command: "track",
|
|
||||||
description: "添加一个新的追踪项目 - 支持 Emmet 语法:track npc#john.dwarf.warrior[hp=4/4 ac=15]",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "emmet",
|
|
||||||
description: "Emmet 格式的追踪项目:tag#class1.class2[attr=value]",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args, commands) => {
|
|
||||||
const emmet = args.params.emmet;
|
|
||||||
if (!emmet) {
|
|
||||||
return { message: "错误:缺少 Emmet 参数", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseEmmet(emmet);
|
|
||||||
|
|
||||||
if (!parsed.tag) {
|
|
||||||
return { message: "错误:缺少 tag", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接添加追踪项目
|
|
||||||
addTrackerItem(parsed);
|
|
||||||
|
|
||||||
const idStr = parsed.id ? ` #${parsed.id}` : "";
|
|
||||||
const classStr = parsed.classes.length > 0 ? `.${parsed.classes.join(".")}` : "";
|
|
||||||
const attrCount = Object.keys(parsed.attributes).length;
|
|
||||||
const attrStr = attrCount > 0 ? `[${attrCount} 个属性]` : "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: `已添加追踪项目:${parsed.tag}${idStr}${classStr}${attrStr}`,
|
|
||||||
type: "success",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const untrackCommand: MdCommanderCommand = {
|
|
||||||
command: "untrack",
|
|
||||||
description: "移除一个追踪项目 - 支持 Emmet 语法:untrack #g2 或 untrack goblin.warrior",
|
|
||||||
parameters: [
|
|
||||||
{
|
|
||||||
name: "emmet",
|
|
||||||
description: "Emmet 格式:#id 或 tag.class",
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
handler: (args) => {
|
|
||||||
const emmet = args.params.emmet;
|
|
||||||
if (!emmet) {
|
|
||||||
return { message: "错误:缺少 Emmet 参数", type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = findTrackerItem(emmet);
|
|
||||||
if (!found) {
|
|
||||||
return { message: `未找到匹配的追踪项目:${emmet}`, type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = removeTrackerItem(emmet);
|
|
||||||
if (!success) {
|
|
||||||
return { message: `移除失败:${emmet}`, type: "error" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: `已移除追踪项目:${found.tag}${found.id ? '#' + found.id : ''}`, type: "success" };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listTrackCommand: MdCommanderCommand = {
|
|
||||||
command: "list",
|
|
||||||
description: "列出所有追踪项目",
|
|
||||||
handler: (args, commands) => {
|
|
||||||
const items = getTrackerItems();
|
|
||||||
if (items.length === 0) {
|
|
||||||
return { message: "暂无追踪项目", type: "info" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = items.map((i) => `${i.tag}${i.id ? "#" + i.id : ""}${i.classes.length > 0 ? "." + i.classes.join(".") : ""}`).join("\n");
|
|
||||||
return { message: `追踪项目:\n${list}`, type: "info" };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import type { MdCommanderCommandMap, CompletionItem } from "../types";
|
|
||||||
|
|
||||||
export interface ParsedCommand {
|
|
||||||
command?: string;
|
|
||||||
params: Record<string, string>;
|
|
||||||
options: Record<string, string>;
|
|
||||||
incompleteParam?: { index: number; value: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析命令行输入
|
|
||||||
*/
|
|
||||||
export function parseInput(input: string, commands?: MdCommanderCommandMap): ParsedCommand {
|
|
||||||
const result: ParsedCommand = { params: {}, options: {} };
|
|
||||||
const trimmed = input.trim();
|
|
||||||
if (!trimmed) return result;
|
|
||||||
|
|
||||||
const parts = trimmed.split(/\s+/);
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
// 获取命令
|
|
||||||
if (parts[0] && !parts[0].startsWith("-")) {
|
|
||||||
result.command = parts[0];
|
|
||||||
i = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取命令的参数定义
|
|
||||||
const cmd = result.command ? commands?.[result.command] : undefined;
|
|
||||||
const paramDefs = cmd?.parameters || [];
|
|
||||||
let paramIndex = 0;
|
|
||||||
|
|
||||||
// 解析参数和选项
|
|
||||||
while (i < parts.length) {
|
|
||||||
const part = parts[i];
|
|
||||||
|
|
||||||
if (part.startsWith("--")) {
|
|
||||||
const eqIndex = part.indexOf("=");
|
|
||||||
if (eqIndex !== -1) {
|
|
||||||
result.options[part.slice(2, eqIndex)] = part.slice(eqIndex + 1);
|
|
||||||
} else {
|
|
||||||
const key = part.slice(2);
|
|
||||||
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
|
||||||
result.options[key] = parts[i + 1];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (part.startsWith("-") && part.length === 2) {
|
|
||||||
const key = part.slice(1);
|
|
||||||
if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) {
|
|
||||||
result.options[key] = parts[i + 1];
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else if (paramIndex < paramDefs.length) {
|
|
||||||
result.params[paramDefs[paramIndex].name] = part;
|
|
||||||
paramIndex++;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有未完成的位置参数
|
|
||||||
if (paramIndex < paramDefs.length) {
|
|
||||||
const lastPart = parts[parts.length - 1];
|
|
||||||
if (lastPart && !lastPart.startsWith("-") && parts.length > 1) {
|
|
||||||
result.incompleteParam = { index: paramIndex, value: lastPart };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取自动补全建议
|
|
||||||
*/
|
|
||||||
export function getCompletions(input: string, commands: MdCommanderCommandMap): CompletionItem[] {
|
|
||||||
const trimmed = input.trim();
|
|
||||||
|
|
||||||
// 空输入时返回所有命令
|
|
||||||
if (!trimmed || /^\s*$/.test(trimmed)) {
|
|
||||||
return Object.values(commands).map((cmd) => ({
|
|
||||||
label: cmd.command,
|
|
||||||
type: "command",
|
|
||||||
description: cmd.description,
|
|
||||||
insertText: cmd.command,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parseInput(trimmed, commands);
|
|
||||||
|
|
||||||
// 命令补全
|
|
||||||
if (!parsed.command || !commands[parsed.command]) {
|
|
||||||
return Object.values(commands)
|
|
||||||
.filter((cmd) => cmd.command.startsWith(parsed.command || ""))
|
|
||||||
.map((cmd) => ({
|
|
||||||
label: cmd.command,
|
|
||||||
type: "command",
|
|
||||||
description: cmd.description,
|
|
||||||
insertText: cmd.command,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const cmd = commands[parsed.command];
|
|
||||||
if (!cmd) return [];
|
|
||||||
|
|
||||||
const paramDefs = cmd.parameters || [];
|
|
||||||
const usedParams = Object.keys(parsed.params);
|
|
||||||
|
|
||||||
// 判断是否正在输入最后一个参数
|
|
||||||
const isTypingLastParam = paramDefs.length === usedParams.length &&
|
|
||||||
trimmed.length > 0 &&
|
|
||||||
!trimmed.endsWith(" ") &&
|
|
||||||
!trimmed.split(/\s+/)[trimmed.split(/\s+/).length - 1].startsWith("-");
|
|
||||||
|
|
||||||
// 参数补全
|
|
||||||
if (paramDefs.length > usedParams.length || parsed.incompleteParam || isTypingLastParam) {
|
|
||||||
let paramIndex = parsed.incompleteParam?.index ?? (isTypingLastParam ? paramDefs.length - 1 : usedParams.length);
|
|
||||||
const paramDef = paramDefs[paramIndex];
|
|
||||||
if (!paramDef) return [];
|
|
||||||
|
|
||||||
let currentValue = parsed.incompleteParam?.value || "";
|
|
||||||
if (isTypingLastParam && !parsed.incompleteParam) {
|
|
||||||
const parts = trimmed.split(/\s+/);
|
|
||||||
currentValue = parts[parts.length - 1] || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模板补全
|
|
||||||
if (paramDef.templates) {
|
|
||||||
return paramDef.templates
|
|
||||||
.filter((t) => t.label.toLowerCase().includes(currentValue.toLowerCase()))
|
|
||||||
.map((t) => ({
|
|
||||||
label: t.label,
|
|
||||||
type: "value",
|
|
||||||
description: t.description,
|
|
||||||
insertText: t.insertText,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 枚举补全
|
|
||||||
if (paramDef.type === "enum" && paramDef.values) {
|
|
||||||
return paramDef.values
|
|
||||||
.filter((v) => v.startsWith(currentValue))
|
|
||||||
.map((v) => ({
|
|
||||||
label: v,
|
|
||||||
type: "value",
|
|
||||||
description: paramDef.description,
|
|
||||||
insertText: v,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他类型显示提示
|
|
||||||
return [{
|
|
||||||
label: `<${paramDef.name}>`,
|
|
||||||
type: "value",
|
|
||||||
description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`,
|
|
||||||
insertText: "",
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选项补全
|
|
||||||
if (!cmd.options) return [];
|
|
||||||
|
|
||||||
const usedOptions = Object.keys(parsed.options);
|
|
||||||
return Object.values(cmd.options)
|
|
||||||
.filter((opt) => !usedOptions.includes(opt.option))
|
|
||||||
.map((opt) => ({
|
|
||||||
label: `--${opt.option}`,
|
|
||||||
type: "option",
|
|
||||||
description: opt.description,
|
|
||||||
insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据结果类型获取 CSS 类名
|
|
||||||
*/
|
|
||||||
export function getResultClass(type?: "success" | "error" | "warning" | "info"): string {
|
|
||||||
switch (type) {
|
|
||||||
case "success": return "text-green-600";
|
|
||||||
case "error": return "text-red-600";
|
|
||||||
case "warning": return "text-yellow-600";
|
|
||||||
default: return "text-blue-600";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export { useCommander } from "./useCommander";
|
|
||||||
export type { UseCommanderReturn } from "./useCommander";
|
|
||||||
export { rollFormula } from "../../dice";
|
|
||||||
export type { DiceRollerResult } from "../../dice";
|
|
||||||
export { parseInput, getCompletions, getResultClass } from "./completions";
|
|
||||||
export type { ParsedCommand } from "./completions";
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import { createSignal } from "solid-js";
|
|
||||||
import type {
|
|
||||||
CommanderEntry,
|
|
||||||
CompletionItem,
|
|
||||||
TrackerItem,
|
|
||||||
TrackerAttribute,
|
|
||||||
TrackerViewMode,
|
|
||||||
MdCommanderCommandMap,
|
|
||||||
TrackerCommand,
|
|
||||||
} from "../types";
|
|
||||||
import { parseInput, getCompletions } from "./completions";
|
|
||||||
import {
|
|
||||||
addTrackerItem,
|
|
||||||
getTrackerHistory,
|
|
||||||
getTrackerItems,
|
|
||||||
moveTrackerItem,
|
|
||||||
removeTrackerClass,
|
|
||||||
removeTrackerItem,
|
|
||||||
updateTrackerAttribute,
|
|
||||||
updateTrackerClasses,
|
|
||||||
} from "../stores";
|
|
||||||
import { addEntry, getEntries, clearEntries } from "../stores/entriesStore";
|
|
||||||
|
|
||||||
// ==================== Commander Hook ====================
|
|
||||||
|
|
||||||
export interface UseCommanderReturn {
|
|
||||||
inputValue: () => string;
|
|
||||||
entries: () => CommanderEntry[];
|
|
||||||
showCompletions: () => boolean;
|
|
||||||
completions: () => CompletionItem[];
|
|
||||||
selectedCompletion: () => number;
|
|
||||||
isFocused: () => boolean;
|
|
||||||
setInputValue: (v: string) => void;
|
|
||||||
setShowCompletions: (v: boolean) => void;
|
|
||||||
setSelectedCompletion: (v: number | ((prev: number) => number)) => void;
|
|
||||||
setIsFocused: (v: boolean) => void;
|
|
||||||
handleCommand: () => void;
|
|
||||||
updateCompletions: () => void;
|
|
||||||
acceptCompletion: () => void;
|
|
||||||
commands: () => MdCommanderCommandMap;
|
|
||||||
setCommands: (cmds: MdCommanderCommandMap) => void;
|
|
||||||
historyIndex: () => number;
|
|
||||||
setHistoryIndex: (v: number) => void;
|
|
||||||
commandHistory: () => string[];
|
|
||||||
navigateHistory: (direction: "up" | "down") => void;
|
|
||||||
|
|
||||||
// Tracker 相关
|
|
||||||
viewMode: () => TrackerViewMode;
|
|
||||||
setViewMode: (mode: TrackerViewMode) => void;
|
|
||||||
trackerItems: () => TrackerItem[];
|
|
||||||
trackerHistory: () => TrackerCommand[];
|
|
||||||
addTrackerItem: (item: Omit<TrackerItem, "id">) => TrackerItem;
|
|
||||||
removeTrackerItem: (emmet: string) => boolean;
|
|
||||||
removeTrackerItemByIndex: (index: number) => void;
|
|
||||||
updateTrackerAttribute: (emmet: string, attrName: string, attr: TrackerAttribute) => boolean;
|
|
||||||
updateTrackerAttributeByIndex: (index: number, attrName: string, attr: TrackerAttribute) => void;
|
|
||||||
updateTrackerClassesByIndex: (index: number, classes: string[]) => void;
|
|
||||||
moveTrackerItem: (emmet: string, direction: "up" | "down") => boolean;
|
|
||||||
moveTrackerItemByIndex: (index: number, direction: "up" | "down") => void;
|
|
||||||
removeTrackerItemClass: (emmet: string, className: string) => boolean;
|
|
||||||
removeTrackerItemClassByIndex: (index: number, className: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCommander(initialCommands?: MdCommanderCommandMap): UseCommanderReturn {
|
|
||||||
const [inputValue, setInputValue] = createSignal("");
|
|
||||||
const [showCompletions, setShowCompletions] = createSignal(false);
|
|
||||||
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
|
||||||
const [selectedCompletion, setSelectedCompletionState] = createSignal(0);
|
|
||||||
const [isFocused, setIsFocused] = createSignal(false);
|
|
||||||
const [commandHistory, setCommandHistory] = createSignal<string[]>([]);
|
|
||||||
const [historyIndex, setHistoryIndex] = createSignal(-1);
|
|
||||||
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
|
|
||||||
const [commands, setCommands] = createSignal<MdCommanderCommandMap>(initialCommands || {});
|
|
||||||
|
|
||||||
// 从 store 获取 entries
|
|
||||||
const entries = getEntries;
|
|
||||||
|
|
||||||
// ==================== 命令执行 ====================
|
|
||||||
|
|
||||||
const handleCommand = () => {
|
|
||||||
const input = inputValue().trim();
|
|
||||||
if (!input) return;
|
|
||||||
|
|
||||||
const cmds = commands();
|
|
||||||
const parsed = parseInput(input, cmds);
|
|
||||||
const commandName = parsed.command;
|
|
||||||
const cmd = cmds[commandName!];
|
|
||||||
|
|
||||||
let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean };
|
|
||||||
|
|
||||||
if (!cmd) {
|
|
||||||
result = { message: `未知命令:${commandName}`, type: "error" };
|
|
||||||
} else if (cmd.handler) {
|
|
||||||
try {
|
|
||||||
result = cmd.handler({ params: parsed.params, options: parsed.options }, cmds);
|
|
||||||
} catch (e) {
|
|
||||||
result = {
|
|
||||||
message: `执行错误:${e instanceof Error ? e.message : String(e)}`,
|
|
||||||
type: "error",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result = { message: `命令 ${commandName} 已执行(无处理器)`, type: "info" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const newEntry: CommanderEntry = {
|
|
||||||
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
|
||||||
command: input,
|
|
||||||
args: parsed.options,
|
|
||||||
result,
|
|
||||||
timestamp: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用 store 添加记录
|
|
||||||
addEntry(newEntry);
|
|
||||||
setCommandHistory((prev) => [...prev, input]);
|
|
||||||
setHistoryIndex(-1);
|
|
||||||
setInputValue("");
|
|
||||||
setShowCompletions(false);
|
|
||||||
|
|
||||||
if (commandName === "clear") {
|
|
||||||
clearEntries();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 自动补全 ====================
|
|
||||||
|
|
||||||
const updateCompletions = () => {
|
|
||||||
const comps = getCompletions(inputValue(), commands());
|
|
||||||
setCompletions(comps);
|
|
||||||
setShowCompletions(comps.length > 0 && isFocused());
|
|
||||||
setSelectedCompletionState(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setSelectedCompletion = (v: number | ((prev: number) => number)) => {
|
|
||||||
const maxIdx = completions().length - 1;
|
|
||||||
if (typeof v === "function") {
|
|
||||||
setSelectedCompletionState((prev) => {
|
|
||||||
const next = v(prev);
|
|
||||||
return Math.max(0, Math.min(next, maxIdx));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSelectedCompletionState(Math.max(0, Math.min(v, maxIdx)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const acceptCompletion = () => {
|
|
||||||
const idx = selectedCompletion();
|
|
||||||
const comps = completions();
|
|
||||||
const comp = comps[Math.min(idx, comps.length - 1)];
|
|
||||||
if (!comp) return;
|
|
||||||
|
|
||||||
const input = inputValue();
|
|
||||||
const parsed = parseInput(input, commands());
|
|
||||||
let newValue: string;
|
|
||||||
|
|
||||||
if (comp.type === "command") {
|
|
||||||
newValue = comp.insertText + " ";
|
|
||||||
} else if (comp.type === "option") {
|
|
||||||
const existingOptions = Object.entries(parsed.options)
|
|
||||||
.map(([k, v]) => `--${k}=${v}`)
|
|
||||||
.join(" ");
|
|
||||||
newValue = `${parsed.command || ""} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`;
|
|
||||||
} else if (comp.type === "value") {
|
|
||||||
const cmd = parsed.command ? commands()[parsed.command] : null;
|
|
||||||
const paramDefs = cmd?.parameters || [];
|
|
||||||
const usedParams = Object.keys(parsed.params);
|
|
||||||
const isTypingLastParam = paramDefs.length === usedParams.length && !input.endsWith(" ");
|
|
||||||
|
|
||||||
if (isTypingLastParam) {
|
|
||||||
const parts = input.trim().split(/\s+/);
|
|
||||||
parts.pop();
|
|
||||||
newValue = `${parsed.command || ""}${parts.length > 1 ? " " + parts.slice(1).join(" ") : ""} ${comp.insertText}`;
|
|
||||||
} else if (paramDefs.length > usedParams.length) {
|
|
||||||
const existingParams = Object.values(parsed.params).join(" ");
|
|
||||||
newValue = `${parsed.command || ""} ${existingParams}${existingParams ? " " : ""}${comp.insertText}`;
|
|
||||||
} else {
|
|
||||||
newValue = input;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newValue = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputValue(newValue.trim());
|
|
||||||
setShowCompletions(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== 命令历史 ====================
|
|
||||||
|
|
||||||
const navigateHistory = (direction: "up" | "down") => {
|
|
||||||
const history = commandHistory();
|
|
||||||
if (history.length === 0) return;
|
|
||||||
|
|
||||||
let newIndex = historyIndex();
|
|
||||||
if (direction === "up") {
|
|
||||||
if (newIndex < history.length - 1) newIndex++;
|
|
||||||
} else {
|
|
||||||
if (newIndex > 0) {
|
|
||||||
newIndex--;
|
|
||||||
} else {
|
|
||||||
setInputValue("");
|
|
||||||
setHistoryIndex(-1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHistoryIndex(newIndex);
|
|
||||||
setInputValue(history[history.length - 1 - newIndex]);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==================== Tracker 操作 ====================
|
|
||||||
|
|
||||||
const getEmmetFromIndex = (index: number): string | null => {
|
|
||||||
const items = getTrackerItems();
|
|
||||||
if (index < 0 || index >= items.length) return null;
|
|
||||||
const item = items[index];
|
|
||||||
return `${item.tag}${item.id ? "#" + item.id : ""}${item.classes.length > 0 ? "." + item.classes.join(".") : ""}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrackerItemByIndex = (index: number) => {
|
|
||||||
const emmet = getEmmetFromIndex(index);
|
|
||||||
if (emmet) removeTrackerItem(emmet);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerAttributeByIndex = (index: number, attrName: string, attr: TrackerAttribute) => {
|
|
||||||
const emmet = getEmmetFromIndex(index);
|
|
||||||
if (emmet) updateTrackerAttribute(emmet, attrName, attr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTrackerClassesByIndex = (index: number, classes: string[]) => {
|
|
||||||
const emmet = getEmmetFromIndex(index);
|
|
||||||
if (emmet) updateTrackerClasses(emmet, classes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const moveTrackerItemByIndex = (index: number, direction: "up" | "down") => {
|
|
||||||
const emmet = getEmmetFromIndex(index);
|
|
||||||
if (emmet) moveTrackerItem(emmet, direction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTrackerItemClassByIndex = (index: number, className: string) => {
|
|
||||||
const emmet = getEmmetFromIndex(index);
|
|
||||||
if (emmet) removeTrackerClass(emmet, className);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
inputValue,
|
|
||||||
entries,
|
|
||||||
showCompletions,
|
|
||||||
completions,
|
|
||||||
selectedCompletion,
|
|
||||||
isFocused,
|
|
||||||
setInputValue,
|
|
||||||
setShowCompletions,
|
|
||||||
setSelectedCompletion,
|
|
||||||
setIsFocused,
|
|
||||||
handleCommand,
|
|
||||||
updateCompletions,
|
|
||||||
acceptCompletion,
|
|
||||||
commands,
|
|
||||||
setCommands,
|
|
||||||
historyIndex,
|
|
||||||
setHistoryIndex,
|
|
||||||
commandHistory,
|
|
||||||
navigateHistory,
|
|
||||||
viewMode,
|
|
||||||
setViewMode,
|
|
||||||
trackerItems: getTrackerItems,
|
|
||||||
trackerHistory: getTrackerHistory,
|
|
||||||
addTrackerItem,
|
|
||||||
removeTrackerItem,
|
|
||||||
removeTrackerItemByIndex,
|
|
||||||
updateTrackerAttribute,
|
|
||||||
updateTrackerAttributeByIndex,
|
|
||||||
updateTrackerClassesByIndex,
|
|
||||||
moveTrackerItem,
|
|
||||||
moveTrackerItemByIndex,
|
|
||||||
removeTrackerItemClass: removeTrackerClass,
|
|
||||||
removeTrackerItemClassByIndex,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { customElement, noShadowDOM } from "solid-element";
|
|
||||||
import { onMount, onCleanup, createResource } from "solid-js";
|
|
||||||
import { useCommander } from "./hooks";
|
|
||||||
import { CommanderInput } from "./CommanderInput";
|
|
||||||
import { CommanderEntries } from "./CommanderEntries";
|
|
||||||
import { TrackerView } from "./TrackerView";
|
|
||||||
import { TabBar } from "./TabBar";
|
|
||||||
import type { MdCommanderProps } from "./types";
|
|
||||||
import { loadElementSrc } from "../utils/path";
|
|
||||||
import { initializeCommands, loadCommandTemplatesFromCSV, getCommands, getCommandsLoading, getCommandsError } from "./stores";
|
|
||||||
|
|
||||||
customElement<MdCommanderProps>(
|
|
||||||
"md-commander",
|
|
||||||
{ placeholder: "", class: "", height: "", commandTemplates: "" },
|
|
||||||
(props, { element }) => {
|
|
||||||
noShadowDOM();
|
|
||||||
const { articlePath, rawSrc } = loadElementSrc(element);
|
|
||||||
|
|
||||||
// 初始化命令
|
|
||||||
const commands = initializeCommands(props.commands);
|
|
||||||
const commander = useCommander(commands);
|
|
||||||
|
|
||||||
// 使用 createResource 加载 CSV 模板
|
|
||||||
const [templateData] = createResource(
|
|
||||||
() => (rawSrc ? { path: rawSrc, articlePath } : null),
|
|
||||||
async (paths) => {
|
|
||||||
await loadCommandTemplatesFromCSV(paths.path, paths.articlePath);
|
|
||||||
return getCommands();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 当模板加载完成后更新 commander 中的命令
|
|
||||||
createResource(
|
|
||||||
() => templateData(),
|
|
||||||
(loadedCommands) => {
|
|
||||||
if (loadedCommands) {
|
|
||||||
commander.setCommands(loadedCommands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (commander.showCompletions() && commander.completions().length > 0) {
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.setSelectedCompletion(
|
|
||||||
(prev) => (prev + 1) % commander.completions().length,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowUp") {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.setSelectedCompletion(
|
|
||||||
(prev) => (prev - 1 + commander.completions().length) % commander.completions().length,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.acceptCompletion();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
commander.setShowCompletions(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === "ArrowUp" && !commander.showCompletions()) {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.navigateHistory("up");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowDown" && !commander.showCompletions()) {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.navigateHistory("down");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
commander.handleCommand();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const heightStyle = () => props.height || "400px";
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
if (!element?.contains(e.target as Node)) {
|
|
||||||
commander.setShowCompletions(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("click", handleClickOutside);
|
|
||||||
onCleanup(() => document.removeEventListener("click", handleClickOutside));
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={`w-full flex flex-col border border-gray-300 rounded-lg overflow-hidden mb-4 ${props.class || ""}`}
|
|
||||||
style={{ height: heightStyle() }}
|
|
||||||
>
|
|
||||||
<TabBar mode={commander.viewMode} onModeChange={commander.setViewMode} />
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col lg:flex-row">
|
|
||||||
{/* 历史视图 */}
|
|
||||||
<div class={`flex-1 flex-col min-h-0 lg:flex lg:w-1/2 lg:border-r lg:border-gray-300 ${commander.viewMode() !== "history" ? "hidden" : ""}`}>
|
|
||||||
<CommanderEntries
|
|
||||||
entries={commander.entries}
|
|
||||||
loading={getCommandsLoading()}
|
|
||||||
error={getCommandsError()}
|
|
||||||
onCommandClick={(cmd) => commander.setInputValue(cmd)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 追踪视图 */}
|
|
||||||
<div class={`flex-1 flex-col min-h-0 lg:flex lg:w-1/2 lg:border-r lg:border-gray-300 ${commander.viewMode() !== "tracker" ? "hidden" : ""}`}>
|
|
||||||
<TrackerView
|
|
||||||
items={commander.trackerItems}
|
|
||||||
onEditAttribute={(index, attrName, attr) =>
|
|
||||||
commander.updateTrackerAttributeByIndex(index, attrName, attr)
|
|
||||||
}
|
|
||||||
onClassesChange={(index, classes) =>
|
|
||||||
commander.updateTrackerClassesByIndex(index, classes)
|
|
||||||
}
|
|
||||||
onRemoveClass={(index, className) =>
|
|
||||||
commander.removeTrackerItemClassByIndex(index, className)
|
|
||||||
}
|
|
||||||
onMoveUp={(index) => commander.moveTrackerItemByIndex(index, "up")}
|
|
||||||
onMoveDown={(index) => commander.moveTrackerItemByIndex(index, "down")}
|
|
||||||
onRemove={(index) => commander.removeTrackerItemByIndex(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative border-t border-gray-300">
|
|
||||||
<CommanderInput
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
inputValue={commander.inputValue}
|
|
||||||
onInput={(e) => {
|
|
||||||
commander.setInputValue((e.target as HTMLInputElement).value);
|
|
||||||
commander.updateCompletions();
|
|
||||||
}}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onFocus={() => {
|
|
||||||
commander.setIsFocused(true);
|
|
||||||
commander.updateCompletions();
|
|
||||||
}}
|
|
||||||
onBlur={() => commander.setIsFocused(false)}
|
|
||||||
onSubmit={commander.handleCommand}
|
|
||||||
showCompletions={commander.showCompletions}
|
|
||||||
completions={commander.completions}
|
|
||||||
selectedCompletion={commander.selectedCompletion}
|
|
||||||
onSelectCompletion={commander.setSelectedCompletion}
|
|
||||||
onAcceptCompletion={commander.acceptCompletion}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { createStore } from "solid-js/store";
|
|
||||||
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
|
||||||
import { setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
|
|
||||||
import {resolvePath} from "../../utils/path";
|
|
||||||
import {loadCSVFromPath} from "../../utils/csv-loader";
|
|
||||||
|
|
||||||
const defaultCommands: MdCommanderCommandMap = {
|
|
||||||
help: setupHelpCommand({}),
|
|
||||||
clear: clearCommand,
|
|
||||||
roll: rollCommand,
|
|
||||||
track: trackCommand,
|
|
||||||
untrack: untrackCommand,
|
|
||||||
list: listTrackCommand,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CommandsStoreState {
|
|
||||||
commands: MdCommanderCommandMap;
|
|
||||||
initialized: boolean;
|
|
||||||
loading: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [commandsStore, setCommandsStore] = createStore<CommandsStoreState>({
|
|
||||||
commands: { ...defaultCommands },
|
|
||||||
initialized: false,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化命令(包括 help 命令的动态更新)
|
|
||||||
*/
|
|
||||||
export function initializeCommands(customCommands?: MdCommanderCommandMap): MdCommanderCommandMap {
|
|
||||||
const commands = { ...defaultCommands, ...customCommands };
|
|
||||||
// 更新 help 命令的命令列表
|
|
||||||
commands.help = setupHelpCommand(commands);
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册命令到 store
|
|
||||||
*/
|
|
||||||
export function registerCommands(customCommands?: MdCommanderCommandMap): void {
|
|
||||||
const commands = initializeCommands(customCommands);
|
|
||||||
setCommandsStore({ commands, initialized: true, loading: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 store 获取命令
|
|
||||||
*/
|
|
||||||
export function getCommands(): MdCommanderCommandMap {
|
|
||||||
return commandsStore.commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查命令是否已初始化
|
|
||||||
*/
|
|
||||||
export function isCommandsInitialized(): boolean {
|
|
||||||
return commandsStore.initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取单个命令
|
|
||||||
*/
|
|
||||||
export function getCommand(name: string): MdCommanderCommand | undefined {
|
|
||||||
return commandsStore.commands[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取加载状态
|
|
||||||
*/
|
|
||||||
export function getCommandsLoading(): boolean {
|
|
||||||
return commandsStore.loading;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取错误信息
|
|
||||||
*/
|
|
||||||
export function getCommandsError(): string | undefined {
|
|
||||||
return commandsStore.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置加载状态
|
|
||||||
*/
|
|
||||||
export function setCommandsLoading(loading: boolean): void {
|
|
||||||
setCommandsStore("loading", loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置错误信息
|
|
||||||
*/
|
|
||||||
export function setCommandsError(error?: string): void {
|
|
||||||
setCommandsStore("error", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新命令
|
|
||||||
*/
|
|
||||||
export function updateCommands(updater: (prev: MdCommanderCommandMap) => MdCommanderCommandMap): void {
|
|
||||||
setCommandsStore("commands", (prev) => updater(prev));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从 CSV 文件加载命令模板并更新命令定义
|
|
||||||
*/
|
|
||||||
export async function loadCommandTemplatesFromCSV(
|
|
||||||
path: string,
|
|
||||||
articlePath: string
|
|
||||||
): Promise<void> {
|
|
||||||
setCommandsLoading(true);
|
|
||||||
setCommandsError(undefined);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const csv = await loadCSVFromPath<CommandTemplateRow>(resolvePath(articlePath, path));
|
|
||||||
|
|
||||||
// 按命令分组模板
|
|
||||||
const templatesByCommand = new Map<string, CommandTemplateRow[]>();
|
|
||||||
for (const row of csv) {
|
|
||||||
if (!row.command || !row.label || !row.insertedText) continue;
|
|
||||||
|
|
||||||
if (!templatesByCommand.has(row.command)) {
|
|
||||||
templatesByCommand.set(row.command, []);
|
|
||||||
}
|
|
||||||
templatesByCommand.get(row.command)!.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为每个命令添加模板
|
|
||||||
updateCommands((prev) => {
|
|
||||||
const updated = { ...prev };
|
|
||||||
for (const [commandName, rows] of templatesByCommand.entries()) {
|
|
||||||
const cmd = updated[commandName];
|
|
||||||
if (!cmd || !cmd.parameters) continue;
|
|
||||||
|
|
||||||
const templates = rows.map((row) => ({
|
|
||||||
label: row.label,
|
|
||||||
description: row.description || "",
|
|
||||||
insertText: row.insertedText,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 为每个参数添加模板
|
|
||||||
updated[commandName] = {
|
|
||||||
...cmd,
|
|
||||||
parameters: cmd.parameters.map((param) => ({
|
|
||||||
...param,
|
|
||||||
templates: param.templates ? [...param.templates, ...templates] : templates,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
setCommandsStore("initialized", true);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
setCommandsError(`加载命令模板失败:${errorMessage}`);
|
|
||||||
console.warn(`Error loading command templates from ${path}:`, error);
|
|
||||||
} finally {
|
|
||||||
setCommandsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommandTemplateRow {
|
|
||||||
command: string;
|
|
||||||
parameter: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
insertedText: string;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { createStore } from "solid-js/store";
|
|
||||||
import type { CommanderEntry } from "../types";
|
|
||||||
|
|
||||||
export interface EntriesStore {
|
|
||||||
entries: CommanderEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [entriesStore, setEntriesStore] = createStore<EntriesStore>({
|
|
||||||
entries: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加命令执行记录
|
|
||||||
*/
|
|
||||||
export function addEntry(entry: CommanderEntry): void {
|
|
||||||
setEntriesStore("entries", (prev) => [...prev, entry]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有记录
|
|
||||||
*/
|
|
||||||
export function clearEntries(): void {
|
|
||||||
setEntriesStore("entries", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有记录
|
|
||||||
*/
|
|
||||||
export function getEntries(): CommanderEntry[] {
|
|
||||||
return entriesStore.entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量设置记录
|
|
||||||
*/
|
|
||||||
export function setEntries(entries: CommanderEntry[]): void {
|
|
||||||
setEntriesStore("entries", entries);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
export {
|
|
||||||
addTrackerItem,
|
|
||||||
removeTrackerItem,
|
|
||||||
updateTrackerAttribute,
|
|
||||||
updateTrackerClasses,
|
|
||||||
moveTrackerItem,
|
|
||||||
removeTrackerClass,
|
|
||||||
getTrackerItems,
|
|
||||||
getTrackerHistory,
|
|
||||||
clearTrackerHistory,
|
|
||||||
findTrackerIndex,
|
|
||||||
findTrackerItem,
|
|
||||||
} from "./trackerStore";
|
|
||||||
export type { TrackerStore } from "./trackerStore";
|
|
||||||
|
|
||||||
export {
|
|
||||||
initializeCommands,
|
|
||||||
registerCommands,
|
|
||||||
getCommands,
|
|
||||||
isCommandsInitialized,
|
|
||||||
getCommand,
|
|
||||||
loadCommandTemplatesFromCSV,
|
|
||||||
getCommandsLoading,
|
|
||||||
getCommandsError,
|
|
||||||
setCommandsLoading,
|
|
||||||
setCommandsError,
|
|
||||||
updateCommands,
|
|
||||||
} from "./commandsStore";
|
|
||||||
export type { CommandsStoreState } from "./commandsStore";
|
|
||||||
|
|
||||||
export {
|
|
||||||
addEntry,
|
|
||||||
clearEntries,
|
|
||||||
getEntries,
|
|
||||||
setEntries,
|
|
||||||
} from "./entriesStore";
|
|
||||||
export type { EntriesStore } from "./entriesStore";
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import { createStore } from "solid-js/store";
|
|
||||||
import type { TrackerItem, TrackerAttribute, TrackerCommand } from "../types";
|
|
||||||
import {parseEmmet} from "../utils";
|
|
||||||
|
|
||||||
export interface TrackerStore {
|
|
||||||
items: TrackerItem[];
|
|
||||||
history: TrackerCommand[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tracker, setTracker] = createStore<TrackerStore>({
|
|
||||||
items: [],
|
|
||||||
history: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据 Emmet 语法查找匹配的项目
|
|
||||||
* 支持:
|
|
||||||
* - #id - 匹配 id
|
|
||||||
* - tag - 匹配 tag
|
|
||||||
* - tag.class - 匹配 tag 和 class
|
|
||||||
* - .class - 匹配 class
|
|
||||||
* - [attr=value] - 匹配属性
|
|
||||||
*/
|
|
||||||
export function findTrackerIndex(emmet: string): number {
|
|
||||||
const {tag, id, classes, attributes} = parseEmmet(emmet);
|
|
||||||
|
|
||||||
return tracker.items.findIndex(item => {
|
|
||||||
// 匹配 tag
|
|
||||||
if (tag && item.tag !== tag) return false;
|
|
||||||
// 匹配 id
|
|
||||||
if (id && item.id !== id) return false;
|
|
||||||
// 匹配所有 classes
|
|
||||||
if (classes.length > 0) {
|
|
||||||
for (const cls of classes) {
|
|
||||||
if (!item.classes.includes(cls)) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 匹配属性
|
|
||||||
for(const attrName in attributes){
|
|
||||||
const attr = attributes[attrName];
|
|
||||||
if (item.attributes[attrName] !== attr) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findTrackerItem(emmet: string): TrackerItem | undefined {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
return index >= 0 ? tracker.items[index] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addTrackerItem(item: Omit<TrackerItem, "uuid">): TrackerItem {
|
|
||||||
const newItem: TrackerItem = {
|
|
||||||
...item,
|
|
||||||
};
|
|
||||||
|
|
||||||
setTracker("items", (prev) => [...prev, newItem]);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "add",
|
|
||||||
itemId: newItem.id,
|
|
||||||
data: newItem,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return newItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeTrackerItem(emmet: string): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
|
|
||||||
setTracker("items", (prev) => prev.filter((_, i) => i !== index));
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "remove",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
data: item,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackerAttribute(
|
|
||||||
emmet: string,
|
|
||||||
attrName: string,
|
|
||||||
attr: TrackerAttribute
|
|
||||||
): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
attributeUpdates: { [attrName]: attr },
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function moveTrackerItem(emmet: string, direction: "up" | "down"): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
|
||||||
if (newIndex < 0 || newIndex >= tracker.items.length) return false;
|
|
||||||
|
|
||||||
const newItems = [...tracker.items];
|
|
||||||
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
|
|
||||||
|
|
||||||
setTracker("items", newItems);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "reorder",
|
|
||||||
data: newItems,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeTrackerClass(emmet: string, className: string): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, classes: i.classes.filter((c) => c !== className) }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTrackerClasses(emmet: string, classes: string[]): boolean {
|
|
||||||
const index = findTrackerIndex(emmet);
|
|
||||||
if (index === -1) return false;
|
|
||||||
|
|
||||||
const item = tracker.items[index];
|
|
||||||
setTracker("items", (prev) =>
|
|
||||||
prev.map((i, idx) =>
|
|
||||||
idx === index
|
|
||||||
? { ...i, classes: [...classes] }
|
|
||||||
: i
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setTracker("history", (prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: "update",
|
|
||||||
itemId: `${item.tag}${item.id ? '#' + item.id : ''}`,
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTrackerItems(): TrackerItem[] {
|
|
||||||
return tracker.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTrackerHistory(): TrackerCommand[] {
|
|
||||||
return tracker.history;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearTrackerHistory() {
|
|
||||||
setTracker("history", []);
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import type { Component } from "solid-js";
|
|
||||||
|
|
||||||
// ==================== 组件 Props ====================
|
|
||||||
|
|
||||||
export interface MdCommanderProps {
|
|
||||||
placeholder?: string;
|
|
||||||
class?: string;
|
|
||||||
height?: string;
|
|
||||||
commands?: Record<string, MdCommanderCommand>;
|
|
||||||
commandTemplates?: string | string[]; // CSV 文件路径或路径数组
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 命令定义 ====================
|
|
||||||
|
|
||||||
export interface MdCommanderCommandMap {
|
|
||||||
[key: string]: MdCommanderCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MdCommanderCommand {
|
|
||||||
command: string;
|
|
||||||
description?: string;
|
|
||||||
parameters?: MdCommanderParameter[];
|
|
||||||
options?: Record<string, MdCommanderOption>;
|
|
||||||
handler?: (args: {
|
|
||||||
params: Record<string, string>;
|
|
||||||
options: Record<string, string>;
|
|
||||||
}, commands?: MdCommanderCommandMap) => {
|
|
||||||
message: string;
|
|
||||||
type?: "success" | "error" | "info" | "warning";
|
|
||||||
isHtml?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MdCommanderParameter {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
type: MdCommanderOptionType;
|
|
||||||
required?: boolean;
|
|
||||||
default?: string;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
values?: string[];
|
|
||||||
templates?: MdCommanderTemplate[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MdCommanderTemplate {
|
|
||||||
label: string;
|
|
||||||
description?: string;
|
|
||||||
insertText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MdCommanderOptionType =
|
|
||||||
| "string"
|
|
||||||
| "number"
|
|
||||||
| "enum"
|
|
||||||
| "tag"
|
|
||||||
| "boolean";
|
|
||||||
|
|
||||||
export interface MdCommanderOption {
|
|
||||||
option: string;
|
|
||||||
description?: string;
|
|
||||||
type: MdCommanderOptionType;
|
|
||||||
required?: boolean;
|
|
||||||
default?: any;
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
values?: string[];
|
|
||||||
EntryComponent?: Component<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 命令执行记录 ====================
|
|
||||||
|
|
||||||
export interface CommanderEntry {
|
|
||||||
id: string;
|
|
||||||
command: string;
|
|
||||||
args: Record<string, any>;
|
|
||||||
result: {
|
|
||||||
message: string;
|
|
||||||
type?: "success" | "error" | "warning" | "info";
|
|
||||||
isHtml?: boolean;
|
|
||||||
};
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompletionItem {
|
|
||||||
label: string;
|
|
||||||
type: "command" | "option" | "value";
|
|
||||||
description?: string;
|
|
||||||
insertText: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Tracker ====================
|
|
||||||
|
|
||||||
export type TrackerViewMode = "history" | "tracker";
|
|
||||||
|
|
||||||
export type TrackerAttributeType = "progress" | "count" | "string";
|
|
||||||
|
|
||||||
export interface TrackerAttribute {
|
|
||||||
name: string;
|
|
||||||
type: TrackerAttributeType;
|
|
||||||
value: string | number | { x: number; y: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackerItem {
|
|
||||||
tag: string;
|
|
||||||
id?: string;
|
|
||||||
classes: string[];
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackerCommand {
|
|
||||||
type: "add" | "remove" | "update" | "reorder";
|
|
||||||
itemId?: string;
|
|
||||||
data?: Partial<TrackerItem> | TrackerItem[];
|
|
||||||
attributeUpdates?: Record<string, TrackerAttribute>;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import type { TrackerAttribute } from "../types";
|
|
||||||
|
|
||||||
export interface ParsedEmmet {
|
|
||||||
tag: string;
|
|
||||||
id?: string;
|
|
||||||
classes: string[];
|
|
||||||
attributes: Record<string, TrackerAttribute>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 Emmet 风格的 tracker 语法
|
|
||||||
* 格式:tag#id.class1.class2[attr1=value1 attr2=value2]
|
|
||||||
* 示例:npc#john.dwarf.warrior[hp=4/4 ac=15 name="John the Dwarf"]
|
|
||||||
*/
|
|
||||||
export function parseEmmet(input: string): ParsedEmmet {
|
|
||||||
const result: ParsedEmmet = {
|
|
||||||
tag: "",
|
|
||||||
classes: [],
|
|
||||||
attributes: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!input) return result;
|
|
||||||
|
|
||||||
// 匹配属性部分 [...]
|
|
||||||
const attrMatch = input.match(/\[(.+)\]$/);
|
|
||||||
let attrString: string | undefined;
|
|
||||||
let mainPart: string;
|
|
||||||
|
|
||||||
if (attrMatch) {
|
|
||||||
attrString = attrMatch[1];
|
|
||||||
mainPart = input.slice(0, attrMatch.index);
|
|
||||||
} else {
|
|
||||||
mainPart = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 tag、id 和 classes
|
|
||||||
// 格式:tag#id.class1.class2.class3
|
|
||||||
const tagClassMatch = mainPart.match(/^([^#.\s]+)?(?:#([^.\s]+))?(?:\.([^.]+(?:\.[^.]+)*))?/);
|
|
||||||
|
|
||||||
if (tagClassMatch) {
|
|
||||||
if (tagClassMatch[1]) {
|
|
||||||
result.tag = tagClassMatch[1];
|
|
||||||
}
|
|
||||||
if (tagClassMatch[2]) {
|
|
||||||
// # 后面的是 ID,不是 class
|
|
||||||
result.id = tagClassMatch[2];
|
|
||||||
}
|
|
||||||
if (tagClassMatch[3]) {
|
|
||||||
result.classes.push(...tagClassMatch[3].split("."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析属性
|
|
||||||
if (attrString) {
|
|
||||||
result.attributes = parseAttributes(attrString);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析属性字符串
|
|
||||||
* 支持格式:
|
|
||||||
* - hp=4/4 (progress 类型,自动检测)
|
|
||||||
* - count=5 (count 类型,纯数字)
|
|
||||||
* - name="John" (string 类型,带引号)
|
|
||||||
* - name=John (string 类型,不带引号)
|
|
||||||
*/
|
|
||||||
function parseAttributes(attrString: string): Record<string, TrackerAttribute> {
|
|
||||||
const attributes: Record<string, TrackerAttribute> = {};
|
|
||||||
|
|
||||||
// 匹配键值对:使用,分隔
|
|
||||||
const regex = /(\w+)=(?:"([^"]*)"|'([^']*)'|([^,]+))/g;
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while ((match = regex.exec(attrString)) !== null) {
|
|
||||||
const key = match[1];
|
|
||||||
// 匹配的值可能是第 2、3 或 4 组(取决于引号类型)
|
|
||||||
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
||||||
|
|
||||||
// 自动检测类型
|
|
||||||
let attr: TrackerAttribute;
|
|
||||||
|
|
||||||
// 检查是否是 progress 格式 (x/y)
|
|
||||||
const progressMatch = value.match(/^(\d+)\/(\d+)$/);
|
|
||||||
if (progressMatch) {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "progress",
|
|
||||||
value: { x: parseInt(progressMatch[1]), y: parseInt(progressMatch[2]) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 检查是否是纯数字(count 类型)
|
|
||||||
else if (/^\d+$/.test(value)) {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "count",
|
|
||||||
value: parseInt(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 默认 string 类型
|
|
||||||
else {
|
|
||||||
attr = {
|
|
||||||
name: key,
|
|
||||||
type: "string",
|
|
||||||
value: value.replace(/^["']|["']$/g, ""), // 移除可能的引号
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes[key] = attr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { parseEmmet } from './emmetParser';
|
|
||||||
export type { ParsedEmmet } from './emmetParser';
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
tag: md-commander
|
|
||||||
icon: 📋
|
|
||||||
title: 命令追踪器
|
|
||||||
---
|
|
||||||
|
|
||||||
支持命令历史和游戏状态追踪,使用类 Emmet 语法创建追踪项,支持 CSV 命令模板加载。
|
|
||||||
|
|
||||||
**语法:** `:md-commander[./templates.csv]{选项}`
|
|
||||||
|
|
||||||
**追踪 NPC 血量和防御:**
|
|
||||||
```
|
|
||||||
track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]
|
|
||||||
```
|
|
||||||
|
|
||||||
## 语法规则
|
|
||||||
|
|
||||||
- `#id` 设置 ID
|
|
||||||
- `.class` 添加类别
|
|
||||||
- `[attr=value]` 设置属性
|
|
||||||
|
|
||||||
## 属性类型
|
|
||||||
|
|
||||||
| 格式 | 显示 |
|
|
||||||
|---|---|
|
|
||||||
| `x/y` | 进度条 |
|
|
||||||
| 整数 | 计数器 |
|
|
||||||
| 文本 | 文本字段 |
|
|
||||||
|
|
||||||
## 视图模式
|
|
||||||
|
|
||||||
组件有两个标签页,通过顶部标签栏切换:
|
|
||||||
|
|
||||||
- **历史**:显示命令执行历史,点击可重新填入命令
|
|
||||||
- **追踪**:显示当前追踪的所有项目,支持属性编辑、类管理、排序和删除
|
|
||||||
|
|
||||||
## 键盘快捷键
|
|
||||||
|
|
||||||
| 快捷键 | 说明 |
|
|
||||||
|---|---|
|
|
||||||
| `Enter` | 执行命令 |
|
|
||||||
| `Tab` | 接受自动补全 |
|
|
||||||
| `↑` / `↓` | 补全列表导航 / 命令历史导航 |
|
|
||||||
| `Escape` | 关闭补全菜单 |
|
|
||||||
|
|
||||||
## 属性
|
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `placeholder` | string | `"输入命令..."` | 输入框占位符 |
|
|
||||||
| `class` | string | — | 额外 CSS 类 |
|
|
||||||
| `height` | string | `"400px"` | 组件高度 |
|
|
||||||
| `commandTemplates` | string | — | CSV 模板文件路径 |
|
|
||||||
|
|
||||||
## 使用场景
|
|
||||||
|
|
||||||
用于追踪战斗中的 NPC 血量、AC、状态等游戏信息。
|
|
||||||
Reference in New Issue
Block a user