feat: details
This commit is contained in:
parent
f7eabbdeba
commit
a55f55505c
|
|
@ -1,80 +1,39 @@
|
|||
import type { MdCommanderCommand, MdCommanderCommandMap, TrackerAttributeType } from "../types";
|
||||
import type { MdCommanderCommand, MdCommanderCommandMap } from "../types";
|
||||
import { parseEmmet } from "../utils/emmetParser";
|
||||
import { addTrackerItem, removeTrackerItem, getTrackerItems } from "../stores";
|
||||
|
||||
export const trackCommand: MdCommanderCommand = {
|
||||
command: "track",
|
||||
description: "添加一个新的追踪项目",
|
||||
description: "添加一个新的追踪项目 - 支持 Emmet 语法:track npc#john.dwarf.warrior[hp=4/4 ac=15]",
|
||||
parameters: [
|
||||
{
|
||||
name: "tag",
|
||||
description: "追踪项目的标签",
|
||||
name: "emmet",
|
||||
description: "Emmet 格式的追踪项目:tag#class1.class2[attr=value]",
|
||||
type: "string",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
options: {
|
||||
class: {
|
||||
option: "class",
|
||||
description: "添加类(可多次使用)",
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
attr: {
|
||||
option: "attr",
|
||||
description: "添加属性,格式:name:type:value (type: progress/count/string)",
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
handler: (args, commands) => {
|
||||
const tag = args.params.tag;
|
||||
if (!tag) {
|
||||
return { message: "错误:缺少 tag 参数", type: "error" };
|
||||
const emmet = args.params.emmet;
|
||||
if (!emmet) {
|
||||
return { message: "错误:缺少 Emmet 参数", type: "error" };
|
||||
}
|
||||
|
||||
// 解析属性
|
||||
const attributes: Record<string, any> = {};
|
||||
const attrOption = args.options.attr;
|
||||
const parsed = parseEmmet(emmet);
|
||||
|
||||
if (attrOption) {
|
||||
const attrs = Array.isArray(attrOption) ? attrOption : [attrOption];
|
||||
for (const attrStr of attrs) {
|
||||
const parts = attrStr.split(":");
|
||||
if (parts.length >= 3) {
|
||||
const [name, typeStr, ...valueParts] = parts;
|
||||
const type = typeStr as TrackerAttributeType;
|
||||
const valueStr = valueParts.join(":");
|
||||
|
||||
let value: any;
|
||||
if (type === "progress") {
|
||||
const [x, y] = valueStr.split("/").map(Number);
|
||||
value = { x: x || 0, y: y || 0 };
|
||||
} else if (type === "count") {
|
||||
value = parseInt(valueStr) || 0;
|
||||
} else {
|
||||
value = valueStr;
|
||||
}
|
||||
|
||||
attributes[name] = { name, type, value };
|
||||
}
|
||||
}
|
||||
if (!parsed.tag) {
|
||||
return { message: "错误:缺少 tag", type: "error" };
|
||||
}
|
||||
|
||||
// 解析类
|
||||
const classes: string[] = [];
|
||||
const classOption = args.options.class;
|
||||
if (classOption) {
|
||||
const cls = Array.isArray(classOption) ? classOption : [classOption];
|
||||
classes.push(...cls);
|
||||
}
|
||||
// 直接添加追踪项目
|
||||
addTrackerItem(parsed);
|
||||
|
||||
// 通过全局 commander 对象添加追踪项目
|
||||
const event = new CustomEvent("md-commander-track", {
|
||||
detail: { tag, classes, attributes },
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
const classStr = parsed.classes.length > 0 ? `.${parsed.classes.join(".")}` : "";
|
||||
const attrCount = Object.keys(parsed.attributes).length;
|
||||
const attrStr = attrCount > 0 ? `[${attrCount} 个属性]` : "";
|
||||
|
||||
return {
|
||||
message: `已添加追踪项目:${tag}`,
|
||||
message: `已添加追踪项目:${parsed.tag}${classStr}${attrStr}`,
|
||||
type: "success",
|
||||
};
|
||||
},
|
||||
|
|
@ -97,8 +56,7 @@ export const untrackCommand: MdCommanderCommand = {
|
|||
return { message: "错误:缺少 id 参数", type: "error" };
|
||||
}
|
||||
|
||||
const event = new CustomEvent("md-commander-untrack", { detail: { id } });
|
||||
window.dispatchEvent(event);
|
||||
removeTrackerItem(id);
|
||||
|
||||
return { message: `已移除追踪项目:${id}`, type: "success" };
|
||||
},
|
||||
|
|
@ -108,9 +66,12 @@ export const listTrackCommand: MdCommanderCommand = {
|
|||
command: "list",
|
||||
description: "列出所有追踪项目",
|
||||
handler: (args, commands) => {
|
||||
const event = new CustomEvent("md-commander-list-track");
|
||||
window.dispatchEvent(event);
|
||||
const items = getTrackerItems();
|
||||
if (items.length === 0) {
|
||||
return { message: "暂无追踪项目", type: "info" };
|
||||
}
|
||||
|
||||
return { message: "追踪列表已更新", type: "info" };
|
||||
const list = items.map((i) => `${i.tag}${i.classes.length > 0 ? "." + i.classes.join(".") : ""}`).join("\n");
|
||||
return { message: `追踪项目:\n${list}`, type: "info" };
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createSignal, onCleanup, onMount } from "solid-js";
|
||||
import { createSignal } from "solid-js";
|
||||
import {
|
||||
MdCommanderCommand,
|
||||
MdCommanderCommandMap,
|
||||
|
|
@ -11,6 +11,15 @@ import {
|
|||
} from "../types";
|
||||
import { rollSimple } from "./useDiceRoller";
|
||||
import { helpCommand, setupHelpCommand, clearCommand, rollCommand, trackCommand, untrackCommand, listTrackCommand } from "../commands";
|
||||
import {
|
||||
addTrackerItem as addTracker,
|
||||
removeTrackerItem as removeTracker,
|
||||
updateTrackerAttribute as updateTrackerAttr,
|
||||
moveTrackerItem as moveTracker,
|
||||
removeTrackerClass as removeClassFromTracker,
|
||||
getTrackerItems,
|
||||
getTrackerHistory,
|
||||
} from "../stores";
|
||||
|
||||
// ==================== 默认命令 ====================
|
||||
|
||||
|
|
@ -244,13 +253,12 @@ export interface UseCommanderReturn {
|
|||
removeTrackerItem: (itemId: string) => void;
|
||||
updateTrackerAttribute: (itemId: string, attrName: string, attr: TrackerAttribute) => void;
|
||||
moveTrackerItem: (itemId: string, direction: 'up' | 'down') => void;
|
||||
removeTrackerClass: (itemId: string, className: string) => void;
|
||||
removeTrackerItemClass: (itemId: string, className: string) => void;
|
||||
recordTrackerCommand: (cmd: Omit<TrackerCommand, 'timestamp'>) => void;
|
||||
}
|
||||
|
||||
export function useCommander(
|
||||
customCommands?: MdCommanderCommandMap,
|
||||
element?: HTMLElement,
|
||||
): UseCommanderReturn {
|
||||
const [inputValue, setInputValue] = createSignal("");
|
||||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
||||
|
|
@ -265,8 +273,6 @@ export function useCommander(
|
|||
|
||||
// Tracker 相关
|
||||
const [viewMode, setViewMode] = createSignal<TrackerViewMode>("history");
|
||||
const [trackerItems, setTrackerItemsState] = createSignal<TrackerItem[]>([]);
|
||||
const [trackerHistory, setTrackerHistory] = createSignal<TrackerCommand[]>([]);
|
||||
|
||||
const commands = { ...defaultCommands, ...customCommands };
|
||||
|
||||
|
|
@ -277,38 +283,6 @@ export function useCommander(
|
|||
);
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
if (element) {
|
||||
const handleTrack = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail as { tag: string; classes: string[]; attributes: Record<string, TrackerAttribute> };
|
||||
addTrackerItem({
|
||||
tag: detail.tag,
|
||||
classes: detail.classes || [],
|
||||
attributes: detail.attributes || {},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUntrack = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail as { id: string };
|
||||
removeTrackerItem(detail.id);
|
||||
};
|
||||
|
||||
const handleListTrack = () => {
|
||||
// 切换到 tracker 视图
|
||||
setViewMode("tracker");
|
||||
};
|
||||
|
||||
element.addEventListener("md-commander-track", handleTrack as EventListener);
|
||||
element.addEventListener("md-commander-untrack", handleUntrack as EventListener);
|
||||
element.addEventListener("md-commander-list-track", handleListTrack as EventListener);
|
||||
|
||||
onCleanup(() => {
|
||||
element.removeEventListener("md-commander-track", handleTrack as EventListener);
|
||||
element.removeEventListener("md-commander-untrack", handleUntrack as EventListener);
|
||||
element.removeEventListener("md-commander-list-track", handleListTrack as EventListener);
|
||||
});
|
||||
}
|
||||
|
||||
const handleCommand = () => {
|
||||
const input = inputValue().trim();
|
||||
if (!input) return;
|
||||
|
|
@ -435,84 +409,41 @@ export function useCommander(
|
|||
|
||||
// ==================== Tracker 方法 ====================
|
||||
|
||||
const recordTrackerCommand = (cmd: Omit<TrackerCommand, 'timestamp'>) => {
|
||||
setTrackerHistory((prev) => [
|
||||
...prev,
|
||||
{ ...cmd, timestamp: new Date() },
|
||||
]);
|
||||
};
|
||||
|
||||
const addTrackerItem = (item: Omit<TrackerItem, 'id'>) => {
|
||||
const newItem: TrackerItem = {
|
||||
...item,
|
||||
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
||||
};
|
||||
setTrackerItemsState((prev) => [...prev, newItem]);
|
||||
recordTrackerCommand({
|
||||
type: "add",
|
||||
itemId: newItem.id,
|
||||
data: newItem,
|
||||
});
|
||||
return addTracker(item);
|
||||
};
|
||||
|
||||
const removeTrackerItem = (itemId: string) => {
|
||||
const item = trackerItems().find((i) => i.id === itemId);
|
||||
setTrackerItemsState((prev) => prev.filter((i) => i.id !== itemId));
|
||||
recordTrackerCommand({
|
||||
type: "remove",
|
||||
itemId,
|
||||
data: item,
|
||||
});
|
||||
removeTracker(itemId);
|
||||
};
|
||||
|
||||
const updateTrackerAttribute = (itemId: string, attrName: string, attr: TrackerAttribute) => {
|
||||
setTrackerItemsState((prev) =>
|
||||
prev.map((i) =>
|
||||
i.id === itemId
|
||||
? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
|
||||
: i
|
||||
)
|
||||
);
|
||||
recordTrackerCommand({
|
||||
type: "update",
|
||||
itemId,
|
||||
attributeUpdates: { [attrName]: attr },
|
||||
});
|
||||
updateTrackerAttr(itemId, attrName, attr);
|
||||
};
|
||||
|
||||
const moveTrackerItem = (itemId: string, direction: 'up' | 'down') => {
|
||||
const items = trackerItems();
|
||||
const index = items.findIndex((i) => i.id === itemId);
|
||||
if (index === -1) return;
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= items.length) return;
|
||||
|
||||
const newItems = [...items];
|
||||
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
|
||||
setTrackerItemsState(newItems);
|
||||
recordTrackerCommand({
|
||||
type: "reorder",
|
||||
data: newItems,
|
||||
});
|
||||
moveTracker(itemId, direction);
|
||||
};
|
||||
|
||||
const removeTrackerClass = (itemId: string, className: string) => {
|
||||
setTrackerItemsState((prev) =>
|
||||
prev.map((i) =>
|
||||
i.id === itemId
|
||||
? { ...i, classes: i.classes.filter((c) => c !== className) }
|
||||
: i
|
||||
)
|
||||
);
|
||||
recordTrackerCommand({
|
||||
type: "update",
|
||||
itemId,
|
||||
});
|
||||
const removeTrackerItemClass = (itemId: string, className: string) => {
|
||||
removeClassFromTracker(itemId, className);
|
||||
};
|
||||
|
||||
const trackerItems = () => getTrackerItems();
|
||||
const trackerHistory = () => getTrackerHistory();
|
||||
|
||||
const setTrackerItems = (updater: (prev: TrackerItem[]) => TrackerItem[]) => {
|
||||
setTrackerItemsState(updater);
|
||||
// 直接操作 store
|
||||
const current = getTrackerItems();
|
||||
const updated = updater(current);
|
||||
// 计算差异并记录
|
||||
if (updated.length !== current.length) {
|
||||
// 长度变化,可能是批量操作
|
||||
}
|
||||
};
|
||||
|
||||
const recordTrackerCommand = (cmd: Omit<TrackerCommand, 'timestamp'>) => {
|
||||
// store 已经自动记录
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -544,7 +475,7 @@ export function useCommander(
|
|||
removeTrackerItem,
|
||||
updateTrackerAttribute,
|
||||
moveTrackerItem,
|
||||
removeTrackerClass,
|
||||
removeTrackerItemClass,
|
||||
recordTrackerCommand,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ customElement<MdCommanderProps>(
|
|||
(props, { element }) => {
|
||||
noShadowDOM();
|
||||
|
||||
const commander = useCommander(props.commands, element as unknown as HTMLElement);
|
||||
const commander = useCommander(props.commands);
|
||||
const [editingAttr, setEditingAttr] = createSignal<{
|
||||
itemId: string;
|
||||
attrName: string;
|
||||
|
|
@ -102,7 +102,7 @@ customElement<MdCommanderProps>(
|
|||
onEditAttribute={(itemId, attrName, attr) =>
|
||||
setEditingAttr({ itemId, attrName, attr })
|
||||
}
|
||||
onRemoveClass={commander.removeTrackerClass}
|
||||
onRemoveClass={commander.removeTrackerItemClass}
|
||||
onMoveUp={(itemId) => commander.moveTrackerItem(itemId, "up")}
|
||||
onMoveDown={(itemId) => commander.moveTrackerItem(itemId, "down")}
|
||||
onRemove={commander.removeTrackerItem}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
export {
|
||||
addTrackerItem,
|
||||
removeTrackerItem,
|
||||
updateTrackerAttribute,
|
||||
moveTrackerItem,
|
||||
removeTrackerClass,
|
||||
getTrackerItems,
|
||||
getTrackerHistory,
|
||||
clearTrackerHistory,
|
||||
} from "./trackerStore";
|
||||
export type { TrackerStore } from "./trackerStore";
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { createStore } from "solid-js/store";
|
||||
import type { TrackerItem, TrackerAttribute, TrackerCommand } from "../types";
|
||||
|
||||
export interface TrackerStore {
|
||||
items: TrackerItem[];
|
||||
history: TrackerCommand[];
|
||||
}
|
||||
|
||||
const [tracker, setTracker] = createStore<TrackerStore>({
|
||||
items: [],
|
||||
history: [],
|
||||
});
|
||||
|
||||
export function addTrackerItem(item: Omit<TrackerItem, "id">): TrackerItem {
|
||||
const newItem: TrackerItem = {
|
||||
...item,
|
||||
id: Date.now().toString() + Math.random().toString(36).slice(2),
|
||||
};
|
||||
|
||||
setTracker("items", (prev) => [...prev, newItem]);
|
||||
setTracker("history", (prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "add",
|
||||
itemId: newItem.id,
|
||||
data: newItem,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
return newItem;
|
||||
}
|
||||
|
||||
export function removeTrackerItem(itemId: string) {
|
||||
const item = tracker.items.find((i) => i.id === itemId);
|
||||
|
||||
setTracker("items", (prev) => prev.filter((i) => i.id !== itemId));
|
||||
setTracker("history", (prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "remove",
|
||||
itemId,
|
||||
data: item,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function updateTrackerAttribute(
|
||||
itemId: string,
|
||||
attrName: string,
|
||||
attr: TrackerAttribute
|
||||
) {
|
||||
setTracker("items", (prev) =>
|
||||
prev.map((i) =>
|
||||
i.id === itemId
|
||||
? { ...i, attributes: { ...i.attributes, [attrName]: attr } }
|
||||
: i
|
||||
)
|
||||
);
|
||||
setTracker("history", (prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "update",
|
||||
itemId,
|
||||
attributeUpdates: { [attrName]: attr },
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function moveTrackerItem(itemId: string, direction: "up" | "down") {
|
||||
const index = tracker.items.findIndex((i) => i.id === itemId);
|
||||
if (index === -1) return;
|
||||
|
||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= tracker.items.length) return;
|
||||
|
||||
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(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function removeTrackerClass(itemId: string, className: string) {
|
||||
setTracker("items", (prev) =>
|
||||
prev.map((i) =>
|
||||
i.id === itemId
|
||||
? { ...i, classes: i.classes.filter((c) => c !== className) }
|
||||
: i
|
||||
)
|
||||
);
|
||||
setTracker("history", (prev) => [
|
||||
...prev,
|
||||
{
|
||||
type: "update",
|
||||
itemId,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
export function getTrackerItems(): TrackerItem[] {
|
||||
return tracker.items;
|
||||
}
|
||||
|
||||
export function getTrackerHistory(): TrackerCommand[] {
|
||||
return tracker.history;
|
||||
}
|
||||
|
||||
export function clearTrackerHistory() {
|
||||
setTracker("history", []);
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
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> = {};
|
||||
|
||||
// 匹配键值对:key=value 或 key="value with spaces"
|
||||
const regex = /(\w+)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/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;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { parseEmmet } from './emmetParser';
|
||||
export type { ParsedEmmet } from './emmetParser';
|
||||
10
todo.md
10
todo.md
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## md-commander(./components/md-commander)
|
||||
|
||||
- [ ] create a new file for each command
|
||||
- [ ] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view.
|
||||
- [ ] add a command to update the tracker view.
|
||||
- [x] create a new file for each command
|
||||
- [x] add a tab bar to the top of md-commander. it should switch between the current history view and a new tracker view.
|
||||
- [x] add a command to update the tracker view.
|
||||
|
||||
the tracker view should show a list of currently tracked information.
|
||||
|
||||
|
|
@ -21,3 +21,7 @@ the tracker view should support the following interactions:
|
|||
- removing classes
|
||||
|
||||
each interaction should be implemented as a command entry in the history view.
|
||||
|
||||
### 新增功能
|
||||
|
||||
- [x] 支持 Emmet 简写语法:`track npc#john.dwarf.warrior[hp=4/4 ac=15 name="John"]`
|
||||
Loading…
Reference in New Issue