news 2026/9/10 20:29:12

tldraw Commenting 模块深入指南:解读 @tldraw/commenting 完整公共 API 与画布批注接入实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
tldraw Commenting 模块深入指南:解读 @tldraw/commenting 完整公共 API 与画布批注接入实践

tldraw Commenting 模块深入指南:解读 @tldraw/commenting 完整公共 API 与画布批注接入实践

【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw

在 tldraw 无限画布中为图形、页面任意点甚至矩形区域挂载“评论线程”(Comment Thread),并支持富文本正文、@成员提及、Emoji 表情反应、已读状态、线索聚合与侧栏导航——这套能力由仓库中的 packages/commenting(npm 包名@tldraw/commenting)承载。它是一套开箱即用的协作评论层(batteries-included),直接读写editor.store中的评论记录并在画布上响应式渲染。本文以该包自动生成的公共 API 报告 api-report.api.md 为骨架,对照其源码实现(src 目录),逐层拆解CommentToolCommentingOptionsCommentingContext、记录读写、反应系统与全部 UI 组件,让读者既能按名索骥地查阅每一个导出符号,也能照着示例把评论能力真正接入自己的 tldraw 应用。

一、包概览:两层 API 设计与仓库中的位置

@tldraw/commenting的公共 API 由两层组成,这一分层在 src/index.ts 的导出注释中写得非常明确:

  • 第一层:展示型组件(Presentational)BylineCommentCardCommentComposerCommentThreadCommentsListCommentPinReactionsEmojiPicker等组件不依赖 tldraw,可单独用来搭建自定义评论 UI。为了让公共 API 保持稳定,@tldraw/mentions中的AvatarCommentAuthorMentionMentionListMentionMembercreateMentionSuggestionfilterMentionMembers等符号由本包直接 re-export。
  • 第二层:与 tldraw 强耦合的评论层(tldraw-coupled)。包括CommentTool工具、围绕评论记录封装的一系列响应式 hooks、富文本正文渲染器CommentBody,以及“全家桶”覆盖层组件CanvasComments;这一层直接与Editoreditor.store打交道。

仓库层面,包配置见 package.json:当前仓库中记录的版本为5.3.2,peerDependencies 为react ^18.2.0 || ^19.2.1react-dom同款,Node 要求>=22.12.0。从tldraw_product元数据可以看出它属于Commenting特性、父级归类在tldraw:collaboration下,并被标记为 premium 特性(licenseFlag: FEAT_COMMENTING)——与源码中的useCommentingEnabled钩子(license.ts)以及CanvasComments组件开头的许可证校验逻辑相呼应。

源码目录结构与 API 层对应关系如下:

源码目录承载的公共 API 类别
src/ui全部展示型组件、时间格式化、反应渲染原语
src/canvasCommentTool、记录读写、hooks、覆盖层、状态 atom、权限判定
src/clustering画布内评论聚合/聚簇的内部算法(MST、运行时模型等)

二、数据模型与锚点:评论记录从哪里来

2.1 三种评论记录类型

评论数据以记录(record)形式存放在editor.store中。类型TLCommentRecord是三种记录的联合:

export type TLCommentRecord = TLComment | TLCommentReaction | TLCommentThread
  • TLCommentThread:一条评论线程,携带anchor(锚点)与可选的resolved解析信息;
  • TLComment:线程中的一条消息/评论,携带富文本bodyauthorId与可选的editedAt
  • TLCommentReaction:对某条评论的某个 Emoji 反应。

实现细节(comment-store.ts)说明:这些记录虽然是可选加入的、并不属于TLRecord联合类型,但由于画布要响应式地渲染它们,它们就存放在 editor 的本地 store 中。因为静态类型上editor.storeStore<TLRecord>,所有读取都需要重新解释类型,comment-store.ts把这些“重新解释”收敛到一个边界后面,使调用点保持类型安全。

需要留意的一个实现事实:评论记录被设计为不可变 + 软删除。删除操作只是置isDeleted标记,由服务端负责真正的清除,这一策略贯穿所有 mutation 函数。

2.2 锚点(Anchor)语义

线程的锚点TLCommentAnchor决定了 pin(大头针)落在哪里。通过 thread-state.ts 的源码可以梳理出四种锚点:

  • point:纯粹的页面坐标点;
  • shape:绑定到某个图形。精确(precise)锚点记录在图形自身坐标系内的归一化 (0–1) 偏移;非精确锚点则渲染在配置的impreciseShapeAnchor位置(默认右上角),并通过图形的 page transform 跟随旋转;
  • region:矩形区域锚点,以虚线框 + 位于释放角上的 pin 呈现,pinX/pinY记录 pin 所在角;
  • page:按源码anchorPagePoint的 switch 分支,page类型返回null(即不渲染 pin),作为兼容占位。

与本主题强相关的三个公共函数负责锚点的换算:

  • anchorPagePoint(editor, anchor):算出线程 pin 应落在页面上的哪个点。精确 shape 锚点按其存储的归一化坐标定位,非精确锚点使用impreciseShapeAnchor,都经由 shape 的 page transform 计算,因此 pin 能跟随图形旋转而不是留在包围盒里。
  • shapeAnchorAt(editor, shapeId, page, precise):为一个页面点构造 shape 锚点,把点击点换算成图形自身坐标系中的 (0–1) 偏移。零宽/零高图形退回中心(0.5, 0.5)
  • registerCommentAnchorLifecycle(editor):注册锚点的生命周期处理(例如 pin 所在图形被删除时,将锚点转换为 point 等)。

另有focusThread(editor, thread):打开线程并让它进入视野——必要时先切换页面,再居中其 pin;revealThread(editor, threadOrCommentId)在画布上“揭示”线程(若被聚合成徽标会先放大展开),对应状态可见下文openThreadId

三、入口配置:CommentTool 与 CommentingOptions

3.1 把评论工具注册进编辑器

评论能力的唯一静态入口是CommentTool。它是一个继承StateNode的状态节点(comment-tool.tsx),静态字段:id = 'comment'initial = 'idle',包含三个子状态idlepointingdragging。它的行为是:

  • 按下时立即在指针处打开评论输入框(composer)并让它跟随指针移动,“像贴便签一样”;
  • 松手时决定锚点:压在图形上则用shapeAnchorAt生成图形锚点,否则落一个 point 锚点;
  • enableRegions开启且拖拽超过阈值,则切换进dragging状态绘制区域矩形,松手时生成 region 锚点,pin 落在释放方向对应的角上;
  • 放置只打开 composer,真正写记录发生在评论发布时;工具在 composer 打开期间保持激活,发布后回到 select。

CommentTool接入画布,与 tldraw 既有约定(如ShapeUtil.configure)一致——先在tools里注册配置后的子类,同时用commentToolOverrides注册 UI 入口(图标、快捷键c):

import { Tldraw } from 'tldraw' import { CommentTool, CanvasComments, commentToolOverrides, } from '@tldraw/commenting' function App() { return ( <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} overrides={commentToolOverrides} > <CanvasComments currentUserId={currentUserId} resolveAuthor={resolveAuthor} /> </Tldraw> ) }

相关导出还有:commentTools[CommentTool]数组)和commentToolOverrides: TLUiOverrides。一旦注册,tldraw 的DefaultQuickActionsContent就会显示评论按钮,点击后通过editor.setCurrentTool('comment')进入该工具。

3.2 CommentingOptions:全部可配置项

CommentTool.configure(options)会基于默认值合并出该 editor 的CommentingOptions,注册为工具的options字段;多次configure可以链式叠加,其中components是浅合并而非整体替换,保证后一次配置不会丢掉前一次已经设置的其他组件槽位(见 comment-tool.tsx 的mergeCommentingOptions)。选项是静态配置:只传一次,活的、会变化的值(当前用户、作者解析、已读回调)走CommentingContext(见第四节)。

getCommentingOptions(editor)useCommentingOptions()可随时读取合并结果——前者通过editor.getStateDescendant('comment')找到已注册工具并读取其options,未注册时回退到defaultCommentingOptions;后者是前者的 React hook 封装,由于选项按 editor 固定,useMemo后不需要响应式。

全部选项及默认值defaultCommentingOptions,见 options.ts)汇总如下:

选项类型/默认值含义
history'ignore'(默认)评论写操作如何与 undo 栈交互。默认'ignore',评论刻意不可撤销;'record'在多玩家场景是雷区——撤销一次删除会把协作方已经移除的线程“复活”,仅单机安全。
dragHistoryundefined(默认回退到history专门控制 pin 拖拽改锚点这类“空间编辑”的 history 模式,可合理跟随图形移动进入撤销栈。
enableClusteringtrue相机缩小时把相邻 pin 折叠成计数徽标(count badge)。
allowMultipleReactionstrue同一用户能否对一条评论同时持有多个 Emoji 反应。true是 Slack 模型,各 Emoji 独立开关;false是单选,选新 Emoji 替换旧反应。注意仅客户端强制,服务端两种都收。
isAllowedReaction(token)isAllowedReactionEmoji某个 token 能否被添加为反应,防止脚本客户端写入选择器永远不会提供的垃圾值。移除不受此检查(不在调色板上的反应也必须能清除)。配合自定义ReactionPalette使用。
enableRegionsfalse是否允许拖拽画出“区域锚点”评论(虚线框 + 角落 pin)。关闭时评论只挂在点与图形上。
canCommentundefined当前观看者能否参与评论(写、改、删、解决、拖 pin)。回调在渲染期经useCanComment调用,信号读取会被追踪;抛异常会被记录并按false处理,不让整个评论层崩掉。未设置时,currentUserId非空即允许。
canModifyCommentundefined针对特定记录的写权限(编辑/删除某条评论、删除某条线程),回调入参为CommentModificationContext。未设置时走defaultCanModifyComment(见 5.4)。
impreciseShapeAnchor{ x: 1, y: 0 }非精确图形 pin 落在图形内的归一化 (0–1) 位置,默认右上角。
shouldBePrecise(editor, ctx)() => true落在图形上的评论是精确钉在点击处,还是锚定整图(渲染在impreciseShapeAnchor)。入参ShapeCommentPrecisionContext携带shapeIdpointaltKey。默认总是精确;只影响新放置,已有锚点按存储渲染。
components{}组件覆盖,见 3.3。

CommentModification是三种写操作的可辨识联合:{ action: 'edit-comment', comment }{ action: 'delete-comment', comment }{ action: 'delete-thread', thread }CommentModificationContext = { editor, currentUserId } & CommentModification

权限细节(源码注释原话要点):解析/重开/反应/移动 pin 不属于任何人,所以只受canComment这一个门槛约束;只有带“归属”的写操作才走canModifyCommentcanModifyComment恒在canComment之后被检查——完全无权参与评论的观看者不会获得任何操作按钮。

3.3 CommentingComponents:组件插槽

components: CommentingComponents提供一组组件槽位,每个槽替换内置的一块 UI,留空即保持默认。所有槽的 props 都由源码(options.ts)明确定义:

插槽Props作用
CommentBody{ comment: TLComment }替换默认富文本正文渲染。
PinContent{ thread, comments }替换 pin 内默认的作者首字母内容。
ThreadPreview{ comment }替换侧栏行预览(默认纯文本)。
ThreadRowCommentListItemRenderProps & { thread }替换整个侧栏行。默认CommentListItem已导出——只加个未读圆点/状态徽章时,可直接把 props 展开进它。仅改预览文本用ThreadPreview即可。
ThreadActions{ thread, comments }在打开的线程头部追加额外控件(在自带的 resolve/dismiss 之前),不是替换。宿主提供getThreadHref时已内置“复制链接”。
ReactionContent{ token: string }给定 token 画某个反应的视觉。默认把 token 字符串渲染给系统 Emoji 字体;可改为自定义img/SVG。token 才是存储与同步的内容,这只管怎么画。
ReactionPaletteEmojiPickerProps“添加反应”按钮打开的面板,默认<EmojiPicker>网格。与ReactionContentisAllowedReaction配套。
ReactionTooltipReactionTooltipPropschildren+reactors悬停显示“谁用这个 Emoji 反应过”的气泡,整个外观归它管;只想改措辞就翻译comments.reacted-*字符串。
ComposerFallback{ context: 'pending' \| 'thread' }观看者不能评论时(见canComment)显示在 composer 位置上的替代物。context表示渲染面:打开的线程弹层('thread')或评论工具放置弹层('pending')。未设置时这些面什么都不渲染。

四、宿主接线:CommentingContext

CommentingContext(context.ts)是评论层的“活”输入:当前用户是谁、作者 id 如何变成显示名、已读状态、提及名单。它通过 props 传给每个评论面。要点如下:

export interface CommentingContext { currentUserId: string | null resolveAuthor(id: string): CommentAuthor | undefined onPostComment?(comment: TLComment): void isCommentUnread?(commentId: TLCommentId): boolean onCommentsRead?(commentIds: TLCommentId[]): void getMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]> renderMentionSuggestion?(member: MentionMember): ReactNode getThreadHref?(threadId: TLCommentThreadId): string | undefined }
  • currentUserId:登录用户 id,null表示只读观看者(只有登录用户能发评论)。
  • resolveAuthor:作者 id → 显示信息;解析不到返回undefined
  • onPostComment:任何评论发布后(新线程首条或回复)回调,适合接后端写接口。
  • isCommentUnread/onCommentsRead:已读状态判定;onCommentsRead把打开线程弹层中展示的所有未读评论 id批量上报(一次报告一批,避免每条评论一次写入),依赖isCommentUnread判断哪些未读——宿主借此记录 read receipt。
  • getMentionSuggestions/renderMentionSuggestion:composer 里@查询的成员名单(可同步可异步)与自定义名单行渲染。
  • getThreadHref:线程的宿主 URL。有它时侧栏行渲染为链接,ctrl/cmd 点击、中键可新标签打开;普通点击仍就地选中线程(不跟随 href)。

源码还给出一个重要实践:同一套 context 会被多个面消费(CanvasCommentsCanvasCommentsSidebar同时挂载时),因此应构建一次、spread 进每一处

const commenting: CommentingContext = { currentUserId, resolveAuthor, isCommentUnread } <CanvasComments {...commenting} /> <CanvasCommentsSidebar {...commenting} />

五、记录读写:读取、hooks、mutation 与权限

5.1 非响应式读取函数

comment-store.ts 提供一组类型安全的记录读取,注意“包括软删除记录”与“应渲染的记录”的差别:

函数语义
getCommentRecord(editor, id)按 id 读一条评论记录,非评论记录/不存在返回undefined
getCommentThreads(editor)全部线程,含软删除、已清空、等待服务端清除的——UI 不应该渲染这些,应使用getLiveCommentThreads
getComments(editor)全部评论,同样含软删除残留。
getLiveComments(editor)应渲染的评论:过滤掉isDeleted。计数与列表都要基于它构建。
getLiveCommentThreads(editor)应渲染的线程:未软删除、且仍持有至少一条存活评论。被最后一条评论删除清空的线程会残留到服务端清除落地。
getCommentReactions(editor)当前 store 中所有评论反应。

以上都非响应式;在 React 里需要包useValue,或直接用下面的 hooks。

5.2 响应式 hooks

Hook语义
useComments(editor)/useCommentThreads(editor)响应式读取“存活”评论/线程(useComments另按最旧在前排序)。
useThreadComments(editor, threadId)某条线程下的存活评论。
useCommentReactions(editor, commentId)某条评论上的反应。
useCommentingOptions()读取合并后的选项(见 3.2)。

5.3 Mutation 函数与 history 策略

所有写操作集中在 comment-mutations.ts,统一经由commitCommentMutation(内部函数)在editor.run(..., { history })中提交。核心函数:

函数行为
putCommentRecords(editor, records)按配置的 history 模式写入记录。可用于种子导入或保存编辑结果。
removeCommentRecords(editor, ids)按 id 硬删除。但硬删除很少是你想要的:内置 UI 走软删除,且强制执行每条记录权限的服务端会直接否决硬删除;仅在本地未同步 store 或丢弃反应时使用(见toggleCommentReaction)。
editComment(editor, comment, body)替换评论正文并打上editedAt时间戳,byline 会渲染“(edited)”标记。示例:editComment(editor, comment, toRichText('Actually, make it dashed'))
deleteComment(editor, comment)软删除(置isDeleted)。删除线程的最后一条评论会关闭该线程并留待服务端清除。永远不可撤销(见下)。
deleteThread(editor, thread)软删除整条线程及其会话。
resolveThread(editor, thread, userId)写入resolved: { at, by }。已解决线程保留 pin(对勾样式),从侧栏隐藏直到打开“show resolved”。
reopenThread(editor, thread)清除 resolution(对未解决线程/已消失线程是 no-op)。
toggleCommentReaction(editor, comment, userId, emoji, now?)开关某个 Emoji 反应。

三个容易踩坑的实现细节值得强调:

  1. 所有动词“读最新、写最小”:传给函数的是记录快照,但记录会移动——删除被钉住的图形会把锚点转成 point、重设父级会迁移线程、拖拽会改锚点。直接把调用方快照写回会把这些字段回滚给所有人。因此readLatest总以当前 store 中的版本为准,已消失的记录直接 no-op。
  2. 删除类写操作恒为'ignore':无论history怎么配。因为软删除标记在服务端是“一次写入”,撤销清除它会被服务端否决而不是恢复内容。
  3. history模式冲突会抛错editor.run的 history 选项不可叠加,嵌套 run 会覆盖外层模式,因此嵌套且模式不一致的提交直接抛出(参见commitCommentMutation的错误信息)。

5.4 权限判定:canComment / canModifyComment

提供“一次调用 + React hook”两套形态:

  • getCanComment(editor, currentUserId)/useCanComment(currentUserId):观看者能否参与评论。未配置时等于currentUserId != null;回调抛异常则拒绝。
  • getCanModifyComment(editor, currentUserId, modification)/useCanModifyComment(currentUserId, modification):能否对特定记录执行特定写操作。useCanModifyComment的依赖是[editor, currentUserId, modification.action, record]——评论记录不可变,记录本身变了才是检查对象变了。
  • defaultCanModifyComment(ctx):默认规则是“归属者拥有写权”——评论由其作者编辑/删除、线程由其创建者删除、无身份观看者一概无权。它被导出以便自定义回调“加宽”而非重述默认,例如:
    CommentTool.configure({ canModifyComment: (ctx) => (ctx.action !== 'edit-comment' && isModerator(ctx.currentUserId)) || defaultCanModifyComment(ctx), })

六、评论工具与 UI 状态(atoms & hooks)

评论层有一组基于EditorAtom的全局 UI 状态,全部集中在 state.ts 并带配套读写函数/hooks:

状态值域配套读写配套 hook语义
commentsHiddenbooleantoggleCommentsHidden(editor)useCommentsHidden()pin 是否隐藏(Shift+C 切换,见CanvasComments)。
commentsSidebarOpenbooleantoggleCommentsSidebar(editor)useCommentsSidebarOpen()评论侧栏开合。
openThreadIdnull \| string—(由打开/关闭动作维护)useOpenThreadId()当前打开线程的 id。
sidebarFiltersSidebarFiltersuseSidebarFilters()侧栏过滤条件(见下)。
reveal 待处理请求null \| stringrevealThread(editor, id)useRevealThreadPending()待揭示的线程/评论 id;getRevealThreadPending(editor)为非响应式读取。

SidebarFilters四个布尔字段:onlyCurrentPage(只看当前页)、onlyMine(只看我的)、onlyUnread(只看未读)、showResolved(显示已解决);默认值DEFAULT_SIDEBAR_FILTERS。侧栏排序辅助:SidebarRow{ item, lastActivity })、sortSidebarRows(rows)

七、全家桶覆盖层:CanvasComments 与侧栏

7.1 CanvasComments

CanvasComments(props: CommentingContext)(comments-overlay.tsx)是“装好电池”的评论层:把每条线程钉在其锚点、点击打开线程弹层(带回复 composer)、在评论工具放置处显示 composer,直接读写editor.store。所有可见部分都是CommentTool.configure({ components })的插槽,且其拼装的零件(CommentPinCommentThreadCommentComposer、hooks、工具)全部导出,想自建可以拆开重拼。

源码中值得了解的行为:

  • 许可证门槛:内部先useCommentingEnabled(),未授权时返回null不渲染。
  • 隐藏与快捷键Shift+C切换 pin 可见性(物理键KeyC,在输入框/文本域/contenteditable 内不触发);Escape 折叠打开的线程(捕获阶段,避开 mention picker 打开时)。
  • 打开线程的 pin 特判:无论是否隐藏/已解决过滤,打开中的线程总会渲染,避免从它自己的弹层解决时 pin 在脚下消失;聚簇开启时,打开线程走独立渲染槽,避免弹层被挂载两次。
  • 渲染宿主:通过EditorPortal渲染——pin 落在协作者光标之下、弹层落在 UI 面板之上,而没有任何单一画布层能同时覆盖两者。
  • 聚簇(clustering):开启时由cluster-model计算聚簇表格与淡入淡出节点、pin-stacking处理同锚点叠置(同锚点 pin 在任何缩放下都重合,渲染为一个计数徽标栈),关闭时每个线程渲染自己的 pin。

7.2 CanvasCommentsSidebar 及其配套

  • CanvasCommentsSidebar(props: CanvasCommentsSidebarProps):评论线程的侧栏列表。CanvasCommentsSidebarProps extends Pick<CommentingContext, 'currentUserId' | 'getThreadHref' | 'isCommentUnread' | 'resolveAuthor'>,另有两个展示插槽:empty?: ReactNodeheader?: ReactNode
  • CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }):过滤菜单(按作者、按未读)。
  • CommentsMenuItem()/CommentsOverflowMenu()/CommentsVisibilityToggle():顶栏 UI 的评论菜单项、溢出菜单、可见性开关——通常配合commentToolOverrides一起接入默认 UI。

八、展示型 UI 组件与反应体系

8.1 线程与列表组件

  • CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }):完整线程视图。commentsCommentCardProps[]composerCommentComposerPropsresolvedBanner可渲染已解决横幅。
  • CommentCard({ author, body, date, you, edited, actions, footer }):单条评论卡。you: boolean标识是否本人的评论。
  • CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }):富文本评论输入框。支持受控value: TLRichText@提及建议(同步/异步)、发送标签自定义、空状态下按上箭头回调、leading前置内容。
  • CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem })+CommentListItem(props):可复用列表容器。CommentListItemProps包含idauthorpreviewdateresolvedpagecountselectedreactionshrefCommentListItemRenderProps额外有onSelectresolvedLabel
  • CommentPin({ children, resolved, open })/CountBadge({ count, open })/EmptyState({ message })/SendButton({ label, disabled, onClick })/Byline({ author, date, edited }):基础原子 UI。
  • CommentBody({ richText, resolveName }):把TLRichText渲染成富文本,resolveName把 mention 的 id 解析为显示名(在 UI 层之上封装了默认渲染)。
  • richTextToPlaintext(body, resolveName?):富文本转纯文本(侧栏预览等场景)。
  • formatFullDateTime(iso, locale?)/formatRelativeTime(iso, locale?):时间格式化(相对时间如“3 分钟前”)。
  • isOpenInNewTabClick(e):判断鼠标事件是否为“新标签打开”点击(ctrl/cmd/中键),供链接行使用。

8.2 Emoji 反应体系

反应体系由一条“token 存储”主线和一套分层 UI 组成:

  • 存储与判定DEFAULT_REACTION_EMOJI: string[](默认表情面板)、isAllowedReactionEmoji(emoji, palette?)(默认允许集检查)。
  • 汇总模型ReactionSummaryInput { createdAt, emoji, userId }summarizeReactions(reactions, currentUserId?, resolveName?)ReactionSummary { emoji, count, active, reactors }ReactionReactor { name, you }
  • 单反应Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick })
  • 反应组Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }),返回null条件由内部决定(无反应且不可添加时)。
  • 选择器EmojiPicker({ emoji, selected, onSelect, renderReaction })ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className })(后者接受自定义palette组件)。
  • 默认渲染defaultRenderReaction(token): ReactNodeDefaultReactionTooltip({ reactors, children })DefaultReactionTooltipContent({ reactors })RenderReaction = (token) => ReactNode
  • 画布层接线CommentReactions({ comment, currentUserId, resolveName })(评论上的反应行)、CommentReactionPicker({ comment, currentUserId, emoji })(评论上加反应的按钮,无权时返回null)、hooksuseCommentReactions与命令toggleCommentReaction

九、一个最小可运行示例

综合前八节,把一个完整评论层挂进 tldraw 的最简骨架如下(仅示意接线关系,不涉及存储持久化):

import { useCallback } from 'react' import { Tldraw, type TLComment, type TLCommentThreadId } from 'tldraw' import { CanvasComments, CanvasCommentsSidebar, CommentTool, commentToolOverrides, type CommentAuthor, } from '@tldraw/commenting' const currentUserId = 'user_me' // 作者 id → 显示信息(实际应从成员数据/服务端解析) const resolveAuthor = (id: string): CommentAuthor | undefined => id === currentUserId ? { id, name: 'Me' } : undefined const isCommentUnread = (id: string) => unreadSet.has(id) // 你的已读数据源 const onPostComment = useCallback((comment: TLComment) => { // 在这里把新评论写入你的后端 }, []) const getThreadHref = (threadId: TLCommentThreadId) => `/board?thread=${threadId}` const getMentionSuggestions = (query: string) => memberRoster.filter((m) => m.name.includes(query)) export function CommentableBoard() { return ( <Tldraw tools={[CommentTool.configure({})]} overrides={commentToolOverrides}> <CanvasComments currentUserId={currentUserId} resolveAuthor={resolveAuthor} isCommentUnread={isCommentUnread} onPostComment={onPostComment} getMentionSuggestions={getMentionSuggestions} getThreadHref={getThreadHref} /> <CanvasCommentsSidebar currentUserId={currentUserId} resolveAuthor={resolveAuthor} isCommentUnread={isCommentUnread} getThreadHref={getThreadHref} /> </Tldraw> ) }

要点回顾:CommentTool.configure({})提供画布放置能力,commentToolOverrides提供工具按钮与c快捷键,CanvasComments画 pin/弹层/composer,CanvasCommentsSidebar提供可过滤的线程列表;二者共享同一份CommentingContext

十、查阅完整 API 的入口

若需要逐符号确认签名(含所有 props 的确切可选性),本仓库中的权威清单是 packages/commenting/api-report.api.md(由 API Extractor 自动生成,693 行,全文标注每个导出为@public)。对应实现可在 packages/commenting/src/index.ts 查看分层导出、在 packages/commenting/src/canvas/options.ts 查看选项与权限、在 packages/commenting/src/canvas/comment-mutations.ts 查看全部写操作。src/canvas下还带有成体系的测试文件(如 comment-mutations.test.ts、comment-tool.test.ts、comments-overlay.test.ts、anchor-lifecycle.test.ts),是观察每条 mutation、每个状态机与锚点生命周期真实行为的绝佳参考。

综合来看,@tldraw/commenting的公共 API 划分清晰:展示层零依赖可复用,画布层围绕 store 记录提供工具、状态、hooks 与覆盖层,配置经CommentTool.configure静态注入、运行期数据经CommentingContext传入。理解这四根支柱,就能在任意 React + tldraw 应用中快速落地一套支持富文本、提及、反应、已读与聚簇的协作评论系统。

【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. World's best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 20:28:17

ARM优化例程库源码深度审计:从NEON汇编到芯片适配

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 20:27:40

配电网辐射状约束建模:断线解环方法详解与Matlab实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 20:25:40

智能制造战略合作的关键维度与实施路径

1. 专访背景与核心价值解读"麦斯时代刘剑锋&#xff1a;钻石级合作背后"这个标题本身就蕴含着丰富的商业洞察价值。作为一家在智能制造领域具有标杆地位的企业&#xff0c;麦斯时代选择合作伙伴的标准向来以严苛著称。这次能够达成"钻石级"合作&#xff0c…

作者头像 李华
网站建设 2026/9/10 20:25:35

GEO生成式引擎优化:以E-E-A-T提升AI搜索引用率

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华