Compare commits

...

3 Commits

Author SHA1 Message Date
8f5bf806e1 修正中文看板术语翻译
Some checks failed
Translate / translate (push) Has been cancelled
2026-09-02 23:46:23 +08:00
0f57279c77 修复卡片右键菜单底部定位 2026-08-25 22:02:43 +08:00
ed5b8d1644 fix(mcp): expose board lists and labels 2026-08-24 20:51:22 +08:00
4 changed files with 129 additions and 66 deletions

View File

@@ -1127,7 +1127,7 @@
115
]
],
"translation": "档案委员会"
"translation": "归档看板"
},
"TdfEV7": {
"message": "Archived",
@@ -1552,7 +1552,7 @@
125
]
],
"translation": "董事会"
"translation": "看板"
},
"YhvWXb": {
"message": "Board analytics",
@@ -1564,7 +1564,7 @@
85
]
],
"translation": "板分析"
"translation": "板分析"
},
"i0ZMQl": {
"message": "Board archived",
@@ -1576,7 +1576,7 @@
47
]
],
"translation": "委员会存档"
"translation": "看板已归档"
},
"wE3hGS": {
"message": "Board moved",
@@ -1588,7 +1588,7 @@
33
]
],
"translation": "移动"
"translation": "看板已移动"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
@@ -1600,7 +1600,7 @@
23
]
],
"translation": "板名称不能超过100个字符"
"translation": "板名称不能超过 100 个字符"
},
"R1c3Mq": {
"message": "Board name is required",
@@ -1612,7 +1612,7 @@
22
]
],
"translation": "董事会名称是必需的"
"translation": "看板名称是必需的"
},
"jwI32v": {
"message": "Board not found",
@@ -1628,7 +1628,7 @@
181
]
],
"translation": "找不到板"
"translation": "找不到板"
},
"XNxYxq": {
"message": "Board templates",
@@ -1640,7 +1640,7 @@
116
]
],
"translation": "板模板"
"translation": "板模板"
},
"tbNcu4": {
"message": "Board unarchived",
@@ -1652,7 +1652,7 @@
47
]
],
"translation": "董事会从未归档"
"translation": "看板已取消归档"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
@@ -1664,7 +1664,7 @@
46
]
],
"translation": "“单板URL只能包含字母、数字和连字符"
"translation": "看板 URL 只能包含字母、数字和连字符"
},
"gv5kbo": {
"message": "Board URL cannot exceed 60 characters",
@@ -1676,7 +1676,7 @@
44
]
],
"translation": "“单板URL不能超过60个字符"
"translation": "看板 URL 不能超过 60 个字符"
},
"8+BY11": {
"message": "Board URL copied to clipboard",
@@ -1720,7 +1720,7 @@
132
]
],
"translation": "董事会的可见性"
"translation": "看板可见性"
},
"PTzsxH": {
"message": "Board visibility updated",
@@ -1732,7 +1732,7 @@
50
]
],
"translation": "董事会可见性更新"
"translation": "看板可见性更新"
},
"DaBhdY": {
"translation": "看板",
@@ -1844,7 +1844,7 @@
20
]
],
"translation": "会制作板子"
"translation": "可以创建看板"
},
"6zeSBB": {
"message": "Can create cards",
@@ -1892,7 +1892,7 @@
22
]
],
"translation": "可以删除板"
"translation": "可以删除板"
},
"jNuzV/": {
"message": "Can delete cards",
@@ -1972,7 +1972,7 @@
21
]
],
"translation": "会编辑版面"
"translation": "可以编辑看板"
},
"I+AROe": {
"message": "Can edit cards",
@@ -2116,7 +2116,7 @@
19
]
],
"translation": "可以查看板"
"translation": "可以查看板"
},
"zoEbXq": {
"message": "Can view cards",
@@ -2893,7 +2893,7 @@
149
]
],
"translation": "Trello帐户连接到导入板。"
"translation": "连接您的 Trello 账户以导入板。"
},
"jfC/xh": {
"message": "Contact",
@@ -3009,7 +3009,7 @@
86
]
],
"translation": "控制谁可以查看和编辑您的板。"
"translation": "控制谁可以查看和编辑您的板。"
},
"zQPhSa": {
"message": "Convert to link",
@@ -3033,7 +3033,7 @@
87
]
],
"translation": "板链接"
"translation": "复制看板链接"
},
"F0YmUY": {
"message": "Copy card link",
@@ -3447,7 +3447,7 @@
38
]
],
"translation": "自定义板模板"
"translation": "自定义板模板"
},
"A42Dqn": {
"message": "Custom branding",
@@ -3704,7 +3704,7 @@
148
]
],
"translation": "删除板"
"translation": "删除板"
},
"nabda1": {
"message": "Delete card",
@@ -4245,7 +4245,7 @@
116
]
],
"translation": "编辑URL"
"translation": "编辑看板 URL"
},
"rF8SEQ": {
"message": "Edit comment",
@@ -4838,7 +4838,7 @@
33
]
],
"translation": "你需要的一切,永远免费。无限的棋盘,无限的列表,无限的牌。随时升级。"
"translation": "你需要的一切,永远免费。无限的看板、列表和卡片。随时升级。"
},
"t+R8+P": {
"message": "Execution",
@@ -4902,7 +4902,7 @@
94
]
],
"translation": "创建板失败"
"translation": "创建板失败"
},
"o4sQAg": {
"message": "Failed to create webhook",
@@ -5531,7 +5531,7 @@
44
]
],
"translation": "到董事会"
"translation": "前往看板"
},
"XEoGkQ": {
"message": "Go to homepage",
@@ -5707,7 +5707,7 @@
46
]
],
"translation": "如何导入我的Trello板"
"translation": "如何导入我的 Trello板?"
},
"nol/Fb": {
"message": "How do I invite team members?",
@@ -5871,7 +5871,7 @@
96
]
],
"translation": "导入您的Trello板并开始运行。"
"translation": "导入您的 Trello 看板,立即开始使用。"
},
"c/IAuR": {
"message": "Important",
@@ -5907,7 +5907,7 @@
49
]
],
"translation": "将Trello板导入Kan很容易。您可以按照我们的步指<0>here</0>。"
"translation": "将 Trello板导入 Kan 很容易。您可以按照我们的步指<0>此处</0>操作。"
},
"02i3WU": {
"message": "Imports",
@@ -6881,7 +6881,7 @@
107
]
],
"translation": "移动板"
"translation": "移动板"
},
"VvCMyU": {
"message": "Move board to another workspace",
@@ -6893,7 +6893,7 @@
64
]
],
"translation": "把板子移到另一个工作区"
"translation": "将看板移动到另一个工作区"
},
"51UCsN": {
"message": "Move to another list",
@@ -7314,7 +7314,7 @@
432
]
],
"translation": "未发现板子"
"translation": "未找到看板"
},
"OpNhRn": {
"message": "No credit card required",
@@ -9013,7 +9013,7 @@
116
]
],
"translation": "使用可重复使用的板模板节省时间。"
"translation": "使用可用的板模板节省时间。"
},
"cOh+MI": {
"message": "Screening",
@@ -9037,7 +9037,7 @@
105
]
],
"translation": "搜索板和卡片……"
"translation": "搜索板和卡片……"
},
"66Jmqh": {
"message": "Seat limit reached",
@@ -10173,7 +10173,7 @@
49
]
],
"translation": "董事会已存档。"
"translation": "看板已归档。"
},
"ZWk38w": {
"message": "The board has been moved to {0}.",
@@ -10189,7 +10189,7 @@
34
]
],
"translation": "板已移至{0}。"
"translation": "板已移至{0}。"
},
"C6gv54": {
"message": "The board has been unarchived.",
@@ -10201,7 +10201,7 @@
50
]
],
"translation": "这块板没有存档。"
"translation": "看板已取消归档。"
},
"nKBUeF": {
"message": "The card has been duplicated.",
@@ -10237,7 +10237,7 @@
33
]
],
"translation": "KanTrello的主要区别在于Kan是开源的任何人都可以查看、修改和贡献我们的代码。我们的云产品对个人使用的功能也没有限制而Trello锁定了基本功能,比如你可以在付费墙后面创建的电路板数量。"
"translation": "KanTrello 的主要区别在于Kan 是开源的,任何人都可以查看、修改和贡献代码。我们的云服务对个人使用的功能也不设限制,而 Trello 会将一些基本功能(例如可创建的板数量)设置为付费项目。"
},
"6tDFBe": {
"message": "The member's permissions have been updated.",
@@ -10305,7 +10305,7 @@
50
]
],
"translation": "您的板可见性已设置为{0}。",
"translation": "您的板可见性已设置为{0}。",
"obsolete": true
},
"7ENRJu": {
@@ -10410,7 +10410,7 @@
184
]
],
"translation": "此板是私有的或不存在"
"translation": "此板是私有的或不存在"
},
"wY+6Ye": {
"message": "This board URL has already been taken",
@@ -10970,7 +10970,7 @@
45
]
],
"translation": "无法移动板"
"translation": "无法移动板"
},
"K7k9u3": {
"message": "Unable to move card",
@@ -11070,7 +11070,7 @@
68
]
],
"translation": "无法更新板"
"translation": "无法更新板"
},
"XpcjLO": {
"message": "Unable to update board URL",
@@ -11082,7 +11082,7 @@
72
]
],
"translation": "无法更新URL"
"translation": "无法更新看板 URL"
},
"Wy6pcF": {
"message": "Unable to update board visibility",
@@ -11094,7 +11094,7 @@
57
]
],
"translation": "无法更新电路板可见性"
"translation": "无法更新板可见性"
},
"o9WLwO": {
"message": "Unable to update card",
@@ -11250,7 +11250,7 @@
115
]
],
"translation": "Unarchive董事会"
"translation": "取消归档看板"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
@@ -11334,7 +11334,7 @@
36
]
],
"translation": "无限的董事会"
"translation": "无限的看板"
},
"ZU5IYI": {
"message": "Unlimited boards, unlimited lists, unlimited cards. No credit card required.",
@@ -11346,7 +11346,7 @@
57
]
],
"translation": "无限的棋盘,无限的列表,无限的牌。不需要信用卡。"
"translation": "无限的看板、列表和卡片。不需要信用卡。"
},
"Zz1qkk": {
"message": "Unlimited cards",
@@ -12529,7 +12529,7 @@
68
]
],
"translation": "没有任何其他工作区来移动这个板。"
"translation": "没有其他工作区可供移动此看板。"
},
"h2FKMV": {
"message": "You don't have permission",
@@ -12731,7 +12731,7 @@
382
]
],
"translation": "您的看板已导入。"
"translation": "您的看板已导入。"
},
"LqWW5F": {
"message": "Your display name has been updated.",

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,17 @@
import { t } from "@lingui/core/macro";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import {
HiLink,
HiOutlineArrowRightCircle,
HiOutlineCalendar,
HiOutlineDocumentDuplicate,
HiOutlineTag,
HiOutlineTrash,
HiOutlineUserGroup,
HiOutlineArrowRightCircle,
} from "react-icons/hi2";
import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect";
export type CardContextMenuAction =
| "members"
| "move"
@@ -85,6 +87,55 @@ export function CardContextMenu({
canEdit,
}: CardContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x, y });
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
useIsomorphicLayoutEffect(() => {
const menu = menuRef.current;
if (!menu) return;
const gutter = 8;
const updatePosition = () => {
const { width, height } = menu.getBoundingClientRect();
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
const viewportHeight =
window.visualViewport?.height ?? window.innerHeight;
const maxX = Math.max(gutter, viewportWidth - width - gutter);
const maxY = Math.max(gutter, viewportHeight - height - gutter);
const nextPosition = {
x: Math.min(Math.max(x, gutter), maxX),
y: Math.min(Math.max(y, gutter), maxY),
};
setPosition((currentPosition) =>
currentPosition.x === nextPosition.x &&
currentPosition.y === nextPosition.y
? currentPosition
: nextPosition,
);
};
updatePosition();
const resizeObserver =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(updatePosition)
: null;
resizeObserver?.observe(menu);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
window.visualViewport?.addEventListener("resize", updatePosition);
window.visualViewport?.addEventListener("scroll", updatePosition);
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
window.visualViewport?.removeEventListener("resize", updatePosition);
window.visualViewport?.removeEventListener("scroll", updatePosition);
};
}, [x, y, canEdit, items.length]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -103,13 +154,11 @@ export function CardContextMenu({
};
}, [onClose]);
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
return (
<div
ref={menuRef}
className="fixed z-[200] min-w-[200px] rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: x, top: y }}
className="fixed z-[200] max-h-[calc(100dvh-1rem)] min-w-[min(200px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] overflow-y-auto overflow-x-hidden rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: position.x, top: position.y }}
>
{items.map(({ action, label, icon }) => (
<button
@@ -119,7 +168,7 @@ export function CardContextMenu({
onAction(action);
onClose();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
className="flex w-full items-center gap-2 break-words px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
{icon}
{label}

View File

@@ -91,21 +91,35 @@ export function registerBoardTools(server: McpServer): void {
server.tool(
"create_board",
"Create a new board in a workspace",
"Create a new board in a workspace, optionally with initial lists and labels",
{
workspacePublicId: z.string().describe("The workspace's public ID"),
name: z.string().describe("Board name"),
slug: z.string().optional().describe("URL-friendly slug (auto-generated if omitted)"),
visibility: z
.enum(["public", "private"])
lists: z
.array(z.string().min(1))
.default([])
.describe("Initial list names, for example [\"Backlog\", \"In Progress\", \"Done\"]"),
labels: z
.array(z.string().min(1))
.default([])
.describe("Initial label names, for example [\"Bug\", \"Feature\"]"),
type: z
.enum(["regular", "template"])
.optional()
.describe("Board visibility (default: private)"),
.describe("Board type (defaults to regular)"),
sourceBoardPublicId: z
.string()
.min(12)
.optional()
.describe("Source board public ID when cloning an existing board"),
},
async ({ workspacePublicId, name, slug, visibility }) => {
async ({ workspacePublicId, name, lists, labels, type, sourceBoardPublicId }) => {
const data = await kanRequest("POST", `/workspaces/${workspacePublicId}/boards`, {
name,
slug,
visibility,
lists,
labels,
type,
sourceBoardPublicId,
});
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},