@tldraw/commenting 5.6.0-canary.e9e8ec003426 → 5.6.0-canary.f283055556fc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/canvas/cluster-badge.js +7 -1
- package/dist-cjs/canvas/cluster-badge.js.map +2 -2
- package/dist-cjs/canvas/comment-reactions.js +10 -4
- package/dist-cjs/canvas/comment-reactions.js.map +2 -2
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +8 -2
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +13 -6
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-menu-item.js +4 -1
- package/dist-cjs/canvas/comments-menu-item.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +9 -4
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +14 -5
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +5 -1
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
- package/dist-cjs/canvas/options.js.map +1 -1
- package/dist-cjs/canvas/pending-composer.js +6 -2
- package/dist-cjs/canvas/pending-composer.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +13 -2
- package/dist-cjs/canvas/thread-pin.js.map +2 -2
- package/dist-cjs/canvas/thread-preview.js +4 -1
- package/dist-cjs/canvas/thread-preview.js.map +2 -2
- package/dist-cjs/canvas/thread-stack.js +18 -4
- package/dist-cjs/canvas/thread-stack.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +40 -20
- package/dist-cjs/canvas/thread-view.js.map +2 -2
- package/dist-cjs/index.d.ts +12 -6
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/ui/comment-composer.js +5 -0
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/gesture-focus.js +37 -0
- package/dist-cjs/ui/gesture-focus.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +5 -2
- package/dist-cjs/ui/reaction-picker.js.map +2 -2
- package/dist-cjs/ui/reaction.js +15 -5
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reply-count.js +8 -2
- package/dist-cjs/ui/reply-count.js.map +2 -2
- package/dist-esm/canvas/cluster-badge.mjs +8 -1
- package/dist-esm/canvas/cluster-badge.mjs.map +2 -2
- package/dist-esm/canvas/comment-reactions.mjs +12 -4
- package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +10 -3
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +14 -6
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-menu-item.mjs +5 -2
- package/dist-esm/canvas/comments-menu-item.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +10 -4
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs +21 -6
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +6 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs.map +1 -1
- package/dist-esm/canvas/pending-composer.mjs +7 -2
- package/dist-esm/canvas/pending-composer.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +15 -3
- package/dist-esm/canvas/thread-pin.mjs.map +2 -2
- package/dist-esm/canvas/thread-preview.mjs +5 -1
- package/dist-esm/canvas/thread-preview.mjs.map +2 -2
- package/dist-esm/canvas/thread-stack.mjs +19 -4
- package/dist-esm/canvas/thread-stack.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +42 -21
- package/dist-esm/canvas/thread-view.mjs.map +2 -2
- package/dist-esm/index.d.mts +12 -6
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/ui/comment-composer.mjs +5 -0
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/gesture-focus.mjs +17 -0
- package/dist-esm/ui/gesture-focus.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +6 -2
- package/dist-esm/ui/reaction-picker.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +16 -8
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +8 -2
- package/dist-esm/ui/reply-count.mjs.map +2 -2
- package/package.json +5 -5
- package/src/canvas/cluster-badge.tsx +9 -1
- package/src/canvas/comment-reactions.tsx +18 -7
- package/src/canvas/comment-render.ts +6 -4
- package/src/canvas/comment-tool.test.ts +45 -1
- package/src/canvas/comment-tool.tsx +14 -3
- package/src/canvas/comments-filter-menu.tsx +15 -6
- package/src/canvas/comments-menu-item.tsx +6 -2
- package/src/canvas/comments-overflow-menu.tsx +11 -4
- package/src/canvas/comments-sidebar.tsx +23 -6
- package/src/canvas/comments-visibility-toggle.tsx +7 -2
- package/src/canvas/options.ts +1 -1
- package/src/canvas/pending-composer.tsx +8 -2
- package/src/canvas/thread-pin.tsx +17 -4
- package/src/canvas/thread-preview.tsx +6 -1
- package/src/canvas/thread-stack.tsx +21 -4
- package/src/canvas/thread-view.tsx +44 -22
- package/src/ui/comment-composer.tsx +9 -0
- package/src/ui/gesture-focus.test.ts +45 -0
- package/src/ui/gesture-focus.ts +29 -0
- package/src/ui/reaction-picker.tsx +7 -2
- package/src/ui/reaction.test.tsx +22 -0
- package/src/ui/reaction.tsx +25 -14
- package/src/ui/reply-count.ts +9 -3
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/gesture-focus.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * iOS opens the software keyboard only for a focus that runs inside a user gesture's own task. A\n * focus scheduled for a later frame still moves the caret, so a field can look ready while\n * swallowing every keystroke until the reader taps it a second time.\n *\n * This covers the case where a field is mounted *during* a press \u2014 the comment composer opens on\n * pointer down and settles its anchor on pointer up \u2014 by running `focus` from the native pointerup\n * that ends the same gesture. That is late enough to survive the canvas taking focus for itself on\n * pointer down, and still inside the gesture, so iOS raises the keyboard.\n *\n * A pointerdown arriving first means the mounting gesture had already ended (a field opened from a\n * button, say), so the next release belongs to an unrelated gesture: give up rather than pull the\n * caret somewhere the reader didn't ask for.\n *\n * Returns a function that detaches the listeners; safe to call more than once.\n */\nexport function focusOnGestureEnd(doc: Document, focus: () => void): () => void {\n\tconst stop = () => {\n\t\tdoc.removeEventListener('pointerup', onPointerUp, true)\n\t\tdoc.removeEventListener('pointerdown', stop, true)\n\t}\n\tfunction onPointerUp() {\n\t\tstop()\n\t\tfocus()\n\t}\n\tdoc.addEventListener('pointerup', onPointerUp, true)\n\tdoc.addEventListener('pointerdown', stop, true)\n\treturn stop\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgBO,SAAS,kBAAkB,KAAe,OAA+B;AAC/E,QAAM,OAAO,MAAM;AAClB,QAAI,oBAAoB,aAAa,aAAa,IAAI;AACtD,QAAI,oBAAoB,eAAe,MAAM,IAAI;AAAA,EAClD;AACA,WAAS,cAAc;AACtB,SAAK;AACL,UAAM;AAAA,EACP;AACA,MAAI,iBAAiB,aAAa,aAAa,IAAI;AACnD,MAAI,iBAAiB,eAAe,MAAM,IAAI;AAC9C,SAAO;AACR;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -27,6 +27,9 @@ var import_react = require("react");
|
|
|
27
27
|
var import_tldraw = require("tldraw");
|
|
28
28
|
var import_emoji_picker = require("./emoji-picker");
|
|
29
29
|
var import_icons = require("./icons");
|
|
30
|
+
const messages = (0, import_tldraw.defineMessages)({
|
|
31
|
+
commentsAddReaction: { id: "comments.add-reaction", defaultMessage: "Add reaction" }
|
|
32
|
+
});
|
|
30
33
|
function isMultiSelectPick(event) {
|
|
31
34
|
return !!event?.shiftKey;
|
|
32
35
|
}
|
|
@@ -57,8 +60,8 @@ function ReactionPicker({
|
|
|
57
60
|
import_tldraw.TldrawUiButton,
|
|
58
61
|
{
|
|
59
62
|
type: "icon",
|
|
60
|
-
tooltip: msg(
|
|
61
|
-
title: msg(
|
|
63
|
+
tooltip: msg(messages.commentsAddReaction),
|
|
64
|
+
title: msg(messages.commentsAddReaction),
|
|
62
65
|
className,
|
|
63
66
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.SmileyIcon, {})
|
|
64
67
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/reaction-picker.tsx"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg(
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'\nimport {\n\tdefineMessages,\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\ttlmenus,\n\tuseMaybeEditor,\n\tuseTranslation,\n} from 'tldraw'\nimport { EmojiPicker, type EmojiPickerProps } from './emoji-picker'\nimport { SmileyIcon } from './icons'\nimport { RenderReaction } from './reaction'\n\nconst messages = defineMessages({\n\tcommentsAddReaction: { id: 'comments.add-reaction', defaultMessage: 'Add reaction' },\n})\n\n/** @public */\nexport interface ReactionPickerProps {\n\t/** The emoji to offer. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n\t/** Emoji the current user has already reacted with; shown as pressed in the grid. */\n\tselected?: string[]\n\t/** Called when an emoji is chosen. */\n\tonSelect?(emoji: string): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/**\n\t * What the button opens \u2014 the thing that produces a token. Defaults to `EmojiPicker`, the grid of\n\t * emoji. Swap it for any component taking the same props to offer something else entirely (the\n\t * drawn reactions example replaces it with a canvas you draw in); the props are passed straight\n\t * through either way.\n\t */\n\tpalette?: ComponentType<EmojiPickerProps>\n\t/**\n\t * Identifies the menu in tldraw's global menu registry, which keys open/closed state by id.\n\t * A thread renders one picker per comment, so this must differ per comment \u2014 sharing an id\n\t * makes every picker in the thread open at once. Defaults to a per-instance generated id;\n\t * pass one only for a stabler, more debuggable value.\n\t */\n\tmenuId?: string\n\t/** Class for the trigger button. Defaults to the card-action style, matching the \u22EF button. */\n\tclassName?: string\n}\n\n/**\n * Whether a palette pick asked to keep the palette open (shift held) for multi-select.\n * @internal\n */\nexport function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {\n\treturn !!event?.shiftKey\n}\n\n/**\n * The add-reaction affordance: a smiley button that opens an emoji grid.\n *\n * Anchored to its own button rather than to the reactions row, so the menu keeps its position as\n * reactions are added and the row reflows.\n *\n * Picking an emoji dismisses the grid \u2014 adding and removing alike, since either way the picker has\n * done its job \u2014 and hands focus back to the trigger. Shift-picking keeps the grid open, for\n * choosing several reactions in a row.\n * @public @react\n */\nexport function ReactionPicker({\n\temoji,\n\tselected,\n\tonSelect,\n\trenderReaction,\n\tpalette: Palette = EmojiPicker,\n\tmenuId,\n\tclassName = 'tlui-cmt-thread__action',\n}: ReactionPickerProps) {\n\tconst msg = useTranslation()\n\tconst editor = useMaybeEditor()\n\t// Unique per mounted picker unless the host supplies something stabler \u2014 see `menuId`.\n\tconst generatedId = useId()\n\tconst id = menuId ?? `comment-reactions-${generatedId}`\n\n\t// The dropdown's open state lives in tldraw's global menu registry, so dropping the entry is\n\t// what closes it. The palette's tokens are plain buttons \u2014 a swappable component, not radix\n\t// menu items \u2014 so nothing dismisses the menu on its own.\n\tconst handleSelect = useCallback(\n\t\t(value: string, event?: MouseEvent | KeyboardEvent) => {\n\t\t\tonSelect?.(value)\n\t\t\tif (!isMultiSelectPick(event)) {\n\t\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\n\t\t\t}\n\t\t},\n\t\t[onSelect, id, editor]\n\t)\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id={id}>\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg(messages.commentsAddReaction)}\n\t\t\t\t\ttitle={msg(messages.commentsAddReaction)}\n\t\t\t\t\tclassName={className}\n\t\t\t\t>\n\t\t\t\t\t<SmileyIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"start\" alignOffset={0} sideOffset={4}>\n\t\t\t\t<Palette\n\t\t\t\t\temoji={emoji}\n\t\t\t\t\tselected={selected}\n\t\t\t\t\tonSelect={handleSelect}\n\t\t\t\t\trenderReaction={renderReaction}\n\t\t\t\t/>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+FE;AA/FF,mBAA4F;AAC5F,oBASO;AACP,0BAAmD;AACnD,mBAA2B;AAG3B,MAAM,eAAW,8BAAe;AAAA,EAC/B,qBAAqB,EAAE,IAAI,yBAAyB,gBAAgB,eAAe;AACpF,CAAC;AAkCM,SAAS,kBAAkB,OAA6C;AAC9E,SAAO,CAAC,CAAC,OAAO;AACjB;AAaO,SAAS,eAAe;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS,UAAU;AAAA,EACnB;AAAA,EACA,YAAY;AACb,GAAwB;AACvB,QAAM,UAAM,8BAAe;AAC3B,QAAM,aAAS,8BAAe;AAE9B,QAAM,kBAAc,oBAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,mBAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,8BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,6CAAC,0CAAyB,IACzB;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,SAAS,mBAAmB;AAAA,QACzC,OAAO,IAAI,SAAS,mBAAmB;AAAA,QACvC;AAAA,QAEA,sDAAC,2BAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,4CAAC,6CAA4B,MAAK,UAAS,OAAM,SAAQ,aAAa,GAAG,YAAY,GACpF;AAAA,MAAC;AAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA;AAAA,IACD,GACD;AAAA,KACD;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/ui/reaction.js
CHANGED
|
@@ -26,6 +26,13 @@ __export(reaction_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(reaction_exports);
|
|
27
27
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
28
28
|
var import_tldraw = require("tldraw");
|
|
29
|
+
const messages = (0, import_tldraw.defineMessages)({
|
|
30
|
+
commentsMentionYou: { id: "comments.mention-you", defaultMessage: "You" },
|
|
31
|
+
commentsReacted: {
|
|
32
|
+
id: "comments.reacted",
|
|
33
|
+
defaultMessage: "{count, plural, =1 {{a} reacted} =2 {{a} and {b} reacted} =3 {{a}, {b} and {c} reacted} other {{a}, {b}, {c} and {others, plural, one {# other} other {# others}} reacted}}"
|
|
34
|
+
}
|
|
35
|
+
});
|
|
29
36
|
function defaultRenderReaction(token) {
|
|
30
37
|
return token;
|
|
31
38
|
}
|
|
@@ -61,13 +68,16 @@ function DefaultReactionTooltip({ reactors, children }) {
|
|
|
61
68
|
function DefaultReactionTooltipContent({ reactors }) {
|
|
62
69
|
const msg = (0, import_tldraw.useTranslation)();
|
|
63
70
|
const names = reactors.map(
|
|
64
|
-
(reactor) => reactor.you ? msg(
|
|
71
|
+
(reactor) => reactor.you ? msg(messages.commentsMentionYou) : reactor.name
|
|
65
72
|
);
|
|
66
73
|
if (names.length === 0) return null;
|
|
67
74
|
const [a, b, c] = names;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: msg(messages.commentsReacted, {
|
|
76
|
+
count: names.length,
|
|
77
|
+
others: names.length - 3,
|
|
78
|
+
a,
|
|
79
|
+
b,
|
|
80
|
+
c
|
|
81
|
+
}) });
|
|
72
82
|
}
|
|
73
83
|
//# sourceMappingURL=reaction.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/reaction.tsx"],
|
|
4
|
-
"sourcesContent": ["import { ComponentType, ReactNode } from 'react'\nimport { TldrawUiTooltip, useTranslation } from 'tldraw'\nimport { ReactionReactor } from './reactions'\n\n/** Render a reaction token to its visual \u2014 the emoji glyph by default. @public */\nexport type RenderReaction = (token: string) => ReactNode\n\n/** The default reaction renderer: emits the token string for the OS emoji font to draw. @public */\nexport function defaultRenderReaction(token: string): ReactNode {\n\treturn token\n}\n\n/** @public */\nexport interface ReactionProps {\n\temoji: string\n\tcount: number\n\tactive: boolean\n\t/** How to draw the emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/** Who reacted with this emoji, in reaction order \u2014 shown when the pill is hovered. */\n\treactors: ReactionReactor[]\n\t/** Whether hovering shows the reactor list. Pass false to suppress it \u2014 e.g. while another\n\t * popup menu on the comment is open. Defaults to true. */\n\tenableHoverList?: boolean\n\t/** The hover affordance for the pill, naming who reacted. It receives the reactors and the pill\n\t * itself (as `children`) and returns the whole thing \u2014 so it owns the tooltip, its box, size,\n\t * shape, and position, not just the text inside. Defaults to `DefaultReactionTooltip`, which\n\t * hangs the built-in inline sentence below the pill. */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/** Called when the pill is pressed \u2014 toggles the current user's reaction. */\n\tonClick?(): void\n}\n\n/**\n * A single emoji reaction pill with a count, highlighted when the user reacted. Hovering it shows a\n * tooltip naming who reacted (the current user included), when `enableHoverList` is set.\n * @public @react\n */\nexport function Reaction({\n\temoji,\n\tcount,\n\tactive,\n\treactors,\n\tenableHoverList = true,\n\trenderReaction = defaultRenderReaction,\n\tReactionTooltip = DefaultReactionTooltip,\n\tonClick,\n}: ReactionProps) {\n\t// An inert pill (no handler) is plain content, not a control \u2014 rendering it as a span keeps it\n\t// valid inside an anchor row (e.g. a notification list item) and out of the tab order.\n\tconst Tag = onClick ? 'button' : 'span'\n\tconst pill = (\n\t\t<Tag\n\t\t\tclassName={active ? 'tlui-cmt-reaction tlui-cmt-reaction--active' : 'tlui-cmt-reaction'}\n\t\t\t{...(onClick\n\t\t\t\t? { type: 'button' as const, 'aria-pressed': active, onClick }\n\t\t\t\t: { role: 'img' as const })}\n\t\t\t// The emoji alone announces as its unicode name (\"thumbs up\"); pairing it with the count\n\t\t\t// is what makes the pill's state legible to a screen reader. The inert span needs the\n\t\t\t// img role for the label to be announced at all.\n\t\t\taria-label={`${emoji} ${count}`}\n\t\t>\n\t\t\t<span className=\"tlui-cmt-reaction__emoji\">{renderReaction(emoji)}</span>\n\t\t\t<span className=\"tlui-cmt-reaction__count\">{count}</span>\n\t\t</Tag>\n\t)\n\n\t// A bare pill when hovering is suppressed, or when there's no one to name.\n\tif (!enableHoverList || reactors.length === 0) return pill\n\n\t// Hand the whole hover affordance to the tooltip component: it wraps the pill and owns how the\n\t// reactor list is presented.\n\treturn <ReactionTooltip reactors={reactors}>{pill}</ReactionTooltip>\n}\n\n/** @public */\nexport interface ReactionTooltipProps {\n\t/** Who reacted with this emoji, in reaction order. */\n\treactors: ReactionReactor[]\n\t/** The reaction pill to wrap. Anchor the hover affordance to this and render it. */\n\tchildren: ReactNode\n}\n\n/**\n * The default reactor tooltip: hangs the built-in inline sentence (`DefaultReactionTooltipContent`)\n * in a standard tooltip below the pill \u2014 the same tooltip the toolbar uses. Replace it via\n * `CommentingComponents.ReactionTooltip` to present the reactor list any other way (a different box,\n * avatars, a banner, anywhere on screen); it receives the pill as `children`.\n * @public @react\n */\nexport function DefaultReactionTooltip({ reactors, children }: ReactionTooltipProps) {\n\treturn (\n\t\t<TldrawUiTooltip side=\"bottom\" content={<DefaultReactionTooltipContent reactors={reactors} />}>\n\t\t\t{children}\n\t\t</TldrawUiTooltip>\n\t)\n}\n\n/**\n * The default reactor sentence naming who reacted \u2014 up to three names spelled out, then \"and N\n * others\" (e.g. \"You reacted\", \"You and Bo reacted\", \"You, Bo and Ada reacted\", \"You, Bo, Ada and 2\n * others reacted\"). The wording lives in the `comments.reacted
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { ComponentType, ReactNode } from 'react'\nimport { defineMessages, TldrawUiTooltip, useTranslation } from 'tldraw'\nimport { ReactionReactor } from './reactions'\n\nconst messages = defineMessages({\n\tcommentsMentionYou: { id: 'comments.mention-you', defaultMessage: 'You' },\n\tcommentsReacted: {\n\t\tid: 'comments.reacted',\n\t\tdefaultMessage:\n\t\t\t'{count, plural, =1 {{a} reacted} =2 {{a} and {b} reacted} =3 {{a}, {b} and {c} reacted} other {{a}, {b}, {c} and {others, plural, one {# other} other {# others}} reacted}}',\n\t},\n})\n\n/** Render a reaction token to its visual \u2014 the emoji glyph by default. @public */\nexport type RenderReaction = (token: string) => ReactNode\n\n/** The default reaction renderer: emits the token string for the OS emoji font to draw. @public */\nexport function defaultRenderReaction(token: string): ReactNode {\n\treturn token\n}\n\n/** @public */\nexport interface ReactionProps {\n\temoji: string\n\tcount: number\n\tactive: boolean\n\t/** How to draw the emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n\t/** Who reacted with this emoji, in reaction order \u2014 shown when the pill is hovered. */\n\treactors: ReactionReactor[]\n\t/** Whether hovering shows the reactor list. Pass false to suppress it \u2014 e.g. while another\n\t * popup menu on the comment is open. Defaults to true. */\n\tenableHoverList?: boolean\n\t/** The hover affordance for the pill, naming who reacted. It receives the reactors and the pill\n\t * itself (as `children`) and returns the whole thing \u2014 so it owns the tooltip, its box, size,\n\t * shape, and position, not just the text inside. Defaults to `DefaultReactionTooltip`, which\n\t * hangs the built-in inline sentence below the pill. */\n\tReactionTooltip?: ComponentType<ReactionTooltipProps>\n\t/** Called when the pill is pressed \u2014 toggles the current user's reaction. */\n\tonClick?(): void\n}\n\n/**\n * A single emoji reaction pill with a count, highlighted when the user reacted. Hovering it shows a\n * tooltip naming who reacted (the current user included), when `enableHoverList` is set.\n * @public @react\n */\nexport function Reaction({\n\temoji,\n\tcount,\n\tactive,\n\treactors,\n\tenableHoverList = true,\n\trenderReaction = defaultRenderReaction,\n\tReactionTooltip = DefaultReactionTooltip,\n\tonClick,\n}: ReactionProps) {\n\t// An inert pill (no handler) is plain content, not a control \u2014 rendering it as a span keeps it\n\t// valid inside an anchor row (e.g. a notification list item) and out of the tab order.\n\tconst Tag = onClick ? 'button' : 'span'\n\tconst pill = (\n\t\t<Tag\n\t\t\tclassName={active ? 'tlui-cmt-reaction tlui-cmt-reaction--active' : 'tlui-cmt-reaction'}\n\t\t\t{...(onClick\n\t\t\t\t? { type: 'button' as const, 'aria-pressed': active, onClick }\n\t\t\t\t: { role: 'img' as const })}\n\t\t\t// The emoji alone announces as its unicode name (\"thumbs up\"); pairing it with the count\n\t\t\t// is what makes the pill's state legible to a screen reader. The inert span needs the\n\t\t\t// img role for the label to be announced at all.\n\t\t\taria-label={`${emoji} ${count}`}\n\t\t>\n\t\t\t<span className=\"tlui-cmt-reaction__emoji\">{renderReaction(emoji)}</span>\n\t\t\t<span className=\"tlui-cmt-reaction__count\">{count}</span>\n\t\t</Tag>\n\t)\n\n\t// A bare pill when hovering is suppressed, or when there's no one to name.\n\tif (!enableHoverList || reactors.length === 0) return pill\n\n\t// Hand the whole hover affordance to the tooltip component: it wraps the pill and owns how the\n\t// reactor list is presented.\n\treturn <ReactionTooltip reactors={reactors}>{pill}</ReactionTooltip>\n}\n\n/** @public */\nexport interface ReactionTooltipProps {\n\t/** Who reacted with this emoji, in reaction order. */\n\treactors: ReactionReactor[]\n\t/** The reaction pill to wrap. Anchor the hover affordance to this and render it. */\n\tchildren: ReactNode\n}\n\n/**\n * The default reactor tooltip: hangs the built-in inline sentence (`DefaultReactionTooltipContent`)\n * in a standard tooltip below the pill \u2014 the same tooltip the toolbar uses. Replace it via\n * `CommentingComponents.ReactionTooltip` to present the reactor list any other way (a different box,\n * avatars, a banner, anywhere on screen); it receives the pill as `children`.\n * @public @react\n */\nexport function DefaultReactionTooltip({ reactors, children }: ReactionTooltipProps) {\n\treturn (\n\t\t<TldrawUiTooltip side=\"bottom\" content={<DefaultReactionTooltipContent reactors={reactors} />}>\n\t\t\t{children}\n\t\t</TldrawUiTooltip>\n\t)\n}\n\n/**\n * The default reactor sentence naming who reacted \u2014 up to three names spelled out, then \"and N\n * others\" (e.g. \"You reacted\", \"You and Bo reacted\", \"You, Bo and Ada reacted\", \"You, Bo, Ada and 2\n * others reacted\"). The wording lives in the `comments.reacted` translation string, whose ICU\n * plural picks the phrasing, so each locale controls both the grammar and how it counts. Exported\n * so a custom `ReactionTooltip` can reuse it inside its own box.\n * @public @react\n */\nexport function DefaultReactionTooltipContent({ reactors }: { reactors: ReactionReactor[] }) {\n\tconst msg = useTranslation()\n\tconst names = reactors.map((reactor) =>\n\t\treactor.you ? msg(messages.commentsMentionYou) : reactor.name\n\t)\n\tif (names.length === 0) return null\n\tconst [a, b, c] = names\n\treturn (\n\t\t<>\n\t\t\t{msg(messages.commentsReacted, {\n\t\t\t\tcount: names.length,\n\t\t\t\tothers: names.length - 3,\n\t\t\t\ta,\n\t\t\t\tb,\n\t\t\t\tc,\n\t\t\t})}\n\t\t</>\n\t)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DE;AA5DF,oBAAgE;AAGhE,MAAM,eAAW,8BAAe;AAAA,EAC/B,oBAAoB,EAAE,IAAI,wBAAwB,gBAAgB,MAAM;AAAA,EACxE,iBAAiB;AAAA,IAChB,IAAI;AAAA,IACJ,gBACC;AAAA,EACF;AACD,CAAC;AAMM,SAAS,sBAAsB,OAA0B;AAC/D,SAAO;AACR;AA4BO,SAAS,SAAS;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB;AACD,GAAkB;AAGjB,QAAM,MAAM,UAAU,WAAW;AACjC,QAAM,OACL;AAAA,IAAC;AAAA;AAAA,MACA,WAAW,SAAS,gDAAgD;AAAA,MACnE,GAAI,UACF,EAAE,MAAM,UAAmB,gBAAgB,QAAQ,QAAQ,IAC3D,EAAE,MAAM,MAAe;AAAA,MAI1B,cAAY,GAAG,KAAK,IAAI,KAAK;AAAA,MAE7B;AAAA,oDAAC,UAAK,WAAU,4BAA4B,yBAAe,KAAK,GAAE;AAAA,QAClE,4CAAC,UAAK,WAAU,4BAA4B,iBAAM;AAAA;AAAA;AAAA,EACnD;AAID,MAAI,CAAC,mBAAmB,SAAS,WAAW,EAAG,QAAO;AAItD,SAAO,4CAAC,mBAAgB,UAAqB,gBAAK;AACnD;AAiBO,SAAS,uBAAuB,EAAE,UAAU,SAAS,GAAyB;AACpF,SACC,4CAAC,iCAAgB,MAAK,UAAS,SAAS,4CAAC,iCAA8B,UAAoB,GACzF,UACF;AAEF;AAUO,SAAS,8BAA8B,EAAE,SAAS,GAAoC;AAC5F,QAAM,UAAM,8BAAe;AAC3B,QAAM,QAAQ,SAAS;AAAA,IAAI,CAAC,YAC3B,QAAQ,MAAM,IAAI,SAAS,kBAAkB,IAAI,QAAQ;AAAA,EAC1D;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAClB,SACC,2EACE,cAAI,SAAS,iBAAiB;AAAA,IAC9B,OAAO,MAAM;AAAA,IACb,QAAQ,MAAM,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC,GACF;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -21,9 +21,15 @@ __export(reply_count_exports, {
|
|
|
21
21
|
replyCountLabel: () => replyCountLabel
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(reply_count_exports);
|
|
24
|
+
var import_tldraw = require("tldraw");
|
|
25
|
+
const messages = (0, import_tldraw.defineMessages)({
|
|
26
|
+
commentsReplies: {
|
|
27
|
+
id: "comments.replies",
|
|
28
|
+
defaultMessage: "{count, plural, one {# reply} other {# replies}}"
|
|
29
|
+
}
|
|
30
|
+
});
|
|
24
31
|
function replyCountLabel(msg, replyCount) {
|
|
25
32
|
if (replyCount <= 0) return null;
|
|
26
|
-
|
|
27
|
-
return msg(key).replace("{count}", String(replyCount));
|
|
33
|
+
return msg(messages.commentsReplies, { count: replyCount });
|
|
28
34
|
}
|
|
29
35
|
//# sourceMappingURL=reply-count.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/reply-count.ts"],
|
|
4
|
-
"sourcesContent": ["import { useTranslation } from 'tldraw'\n\n/**\n * The localized \"N replies\" label (e.g. \"1 reply\", \"3 replies\") for a thread with `replyCount`\n * replies \u2014 the comments after the opening one. Returns null when there are none, so callers render\n * nothing rather than \"0 replies\". Shared by the sidebar row and the hover preview so both read the\n * same, with each site owning where it puts the text.\n */\nexport function replyCountLabel(\n\tmsg: ReturnType<typeof useTranslation>,\n\treplyCount: number\n): string | null {\n\tif (replyCount <= 0) return null\n\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { defineMessages, useTranslation } from 'tldraw'\n\nconst messages = defineMessages({\n\tcommentsReplies: {\n\t\tid: 'comments.replies',\n\t\tdefaultMessage: '{count, plural, one {# reply} other {# replies}}',\n\t},\n})\n\n/**\n * The localized \"N replies\" label (e.g. \"1 reply\", \"3 replies\") for a thread with `replyCount`\n * replies \u2014 the comments after the opening one. Returns null when there are none, so callers render\n * nothing rather than \"0 replies\". Shared by the sidebar row and the hover preview so both read the\n * same, with each site owning where it puts the text.\n */\nexport function replyCountLabel(\n\tmsg: ReturnType<typeof useTranslation>,\n\treplyCount: number\n): string | null {\n\tif (replyCount <= 0) return null\n\treturn msg(messages.commentsReplies, { count: replyCount })\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA+C;AAE/C,MAAM,eAAW,8BAAe;AAAA,EAC/B,iBAAiB;AAAA,IAChB,IAAI;AAAA,IACJ,gBAAgB;AAAA,EACjB;AACD,CAAC;AAQM,SAAS,gBACf,KACA,YACgB;AAChB,MAAI,cAAc,EAAG,QAAO;AAC5B,SAAO,IAAI,SAAS,iBAAiB,EAAE,OAAO,WAAW,CAAC;AAC3D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useMemo, useRef } from "react";
|
|
3
3
|
import {
|
|
4
|
+
defineMessages,
|
|
4
5
|
useContainer,
|
|
5
6
|
usePassThroughWheelEvents,
|
|
6
7
|
useTranslation,
|
|
@@ -10,6 +11,12 @@ import { CountBadge } from "../ui/count-badge.mjs";
|
|
|
10
11
|
import { forwardPointerEventToCanvas, isCanvasPanGesture } from "./canvas-events.mjs";
|
|
11
12
|
import { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from "./thread-preview.mjs";
|
|
12
13
|
import { impreciseShapePinInset, isInInflatedViewport } from "./thread-state.mjs";
|
|
14
|
+
const messages = defineMessages({
|
|
15
|
+
commentsClusterLabel: {
|
|
16
|
+
id: "comments.cluster-label",
|
|
17
|
+
defaultMessage: "{count, plural, one {# comment here} other {# comments here}}"
|
|
18
|
+
}
|
|
19
|
+
});
|
|
13
20
|
const ClusterBadge = memo(function ClusterBadge2({
|
|
14
21
|
editor,
|
|
15
22
|
node,
|
|
@@ -68,7 +75,7 @@ const ClusterBadge = memo(function ClusterBadge2({
|
|
|
68
75
|
type: "button",
|
|
69
76
|
className: "tlui-cmt-button tlui-cmt-canvas-cluster",
|
|
70
77
|
style: { left: point.x, top: point.y },
|
|
71
|
-
"aria-label": msg(
|
|
78
|
+
"aria-label": msg(messages.commentsClusterLabel, { count: node.count }),
|
|
72
79
|
onPointerDown: (e) => {
|
|
73
80
|
if (isCanvasPanGesture(editor, e)) {
|
|
74
81
|
forwardPointerEventToCanvas(container, e);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/cluster-badge.tsx"],
|
|
4
|
-
"sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg(
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { memo, useMemo, useRef } from 'react'\nimport {\n\tdefineMessages,\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { CountBadge } from '../ui/count-badge'\nimport { forwardPointerEventToCanvas, isCanvasPanGesture } from './canvas-events'\nimport { type CommentingContext } from './context'\nimport { sortThreadsForPreview, ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { impreciseShapePinInset, isInInflatedViewport } from './thread-state'\n\nconst messages = defineMessages({\n\tcommentsClusterLabel: {\n\t\tid: 'comments.cluster-label',\n\t\tdefaultMessage: '{count, plural, one {# comment here} other {# comments here}}',\n\t},\n})\n\n/**\n * The count badge standing in for several threads folded together at the current zoom. Hovering it\n * previews its threads as a list; clicking zooms to the zoom level at which the cluster splits.\n *\n * Memoized: cluster nodes and thread records are identity-stable while unchanged, so pins and\n * badges skip re-rendering when the parent re-renders for reasons that don't concern them\n * (leaf recomputes during shape drags, partition changes elsewhere). Camera tracking still\n * works \u2014 each component subscribes to its own viewport position via signals, not via props.\n */\nexport const ClusterBadge = memo(function ClusterBadge({\n\teditor,\n\tnode,\n\tonExpand,\n\tonSelectThread,\n\tthreadsById,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\tnode: ClusterNode\n\tonExpand(node: ClusterNode): void\n\tonSelectThread(thread: TLCommentThread): void\n\tthreadsById: ReadonlyMap<string, TLCommentThread>\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`)\n\t// Wheel pass-through sits on the badge (never scrollable), not the layer root \u2014 see the\n\t// note on the layer.\n\tusePassThroughWheelEvents(badgeRef)\n\t// The mean of the members' render offsets, in screen px. Imprecise members' pins draw tucked\n\t// into their shape (see impreciseShapePinInset), and the merge thresholds are priced at those\n\t// visual positions \u2014 so the badge draws at the centroid of the pins as drawn, which (screen\n\t// mapping being affine) is the anchor centroid plus this mean. Camera-independent: its own\n\t// computed so the per-frame point below only adds two numbers, and a cluster of precise\n\t// members tracks nothing and adds zero.\n\tconst meanInset = useValue(\n\t\t'cluster badge inset',\n\t\t() => {\n\t\t\tlet x = 0\n\t\t\tlet y = 0\n\t\t\tfor (const id of node.members) {\n\t\t\t\tconst thread = threadsById.get(id)\n\t\t\t\tif (!thread) continue\n\t\t\t\tconst inset = impreciseShapePinInset(editor, thread.anchor)\n\t\t\t\tif (inset) {\n\t\t\t\t\tx += inset.x\n\t\t\t\t\ty += inset.y\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { x: x / node.count, y: y / node.count }\n\t\t},\n\t\t[editor, node, threadsById]\n\t)\n\tconst point = useValue(\n\t\t'cluster badge point',\n\t\t() => {\n\t\t\tconst pagePoint = editor.pageToViewport(node.centroid)\n\t\t\tif (!isInInflatedViewport(editor, pagePoint)) return null\n\t\t\treturn { x: pagePoint.x + meanInset.x, y: pagePoint.y + meanInset.y }\n\t\t},\n\t\t[editor, node, meanInset]\n\t)\n\n\t// `node.members` is sorted by id (the clustering table's ordering); the preview wants them in\n\t// the order a reader would expect. Only computed while the preview is up.\n\tconst previewThreads = useMemo(() => {\n\t\tif (!previewShown) return []\n\t\tconst threads: TLCommentThread[] = []\n\t\tfor (const id of node.members) {\n\t\t\tconst thread = threadsById.get(id)\n\t\t\tif (thread) threads.push(thread)\n\t\t}\n\t\treturn sortThreadsForPreview(threads)\n\t}, [previewShown, node.members, threadsById])\n\n\tif (!point) return null\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\tref={badgeRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-cluster\"\n\t\t\t\tstyle={{ left: point.x, top: point.y }}\n\t\t\t\taria-label={msg(messages.commentsClusterLabel, { count: node.count })}\n\t\t\t\tonPointerDown={(e) => {\n\t\t\t\t\tif (isCanvasPanGesture(editor, e)) {\n\t\t\t\t\t\tforwardPointerEventToCanvas(container, e)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t}}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\te.stopPropagation()\n\t\t\t\t\tonExpand(node)\n\t\t\t\t}}\n\t\t\t\t{...previewHandlers}\n\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t>\n\t\t\t\t<CountBadge count={node.count} />\n\t\t\t</button>\n\t\t\t{previewShown && previewThreads.length > 0 && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={previewThreads}\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\tonSelectThread={onSelectThread}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\t{...props}\n\t\t\t\t/>\n\t\t\t)}\n\t\t</>\n\t)\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["ClusterBadge"]
|
|
7
7
|
}
|
|
@@ -3,7 +3,9 @@ import { useMemo } from "react";
|
|
|
3
3
|
import {
|
|
4
4
|
createCommentReaction,
|
|
5
5
|
createCommentReactionId,
|
|
6
|
+
defineMessages,
|
|
6
7
|
useEditor,
|
|
8
|
+
useTranslation,
|
|
7
9
|
useValue
|
|
8
10
|
} from "tldraw";
|
|
9
11
|
import { ReactionPicker } from "../ui/reaction-picker.mjs";
|
|
@@ -12,6 +14,9 @@ import { commitCommentMutation } from "./comment-mutations.mjs";
|
|
|
12
14
|
import { UNKNOWN_AUTHOR } from "./comment-render.mjs";
|
|
13
15
|
import { getCommentReactions } from "./comment-store.mjs";
|
|
14
16
|
import { getCommentingOptions, useCommentingOptions } from "./options.mjs";
|
|
17
|
+
const messages = defineMessages({
|
|
18
|
+
commentsUnknownAuthor: { id: "comments.unknown-author", defaultMessage: "Someone" }
|
|
19
|
+
});
|
|
15
20
|
function useCommentReactions(editor, commentId) {
|
|
16
21
|
return useValue(
|
|
17
22
|
"comment reactions",
|
|
@@ -19,11 +24,12 @@ function useCommentReactions(editor, commentId) {
|
|
|
19
24
|
[editor, commentId]
|
|
20
25
|
);
|
|
21
26
|
}
|
|
22
|
-
function summarizeReactions(reactions, currentUserId, resolveName) {
|
|
27
|
+
function summarizeReactions(reactions, currentUserId, resolveName, opts) {
|
|
28
|
+
const unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR;
|
|
23
29
|
const groups = /* @__PURE__ */ new Map();
|
|
24
30
|
for (const reaction of reactions) {
|
|
25
31
|
const mine = currentUserId != null && reaction.userId === currentUserId;
|
|
26
|
-
const reactor = { name: resolveName?.(reaction.userId) ??
|
|
32
|
+
const reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine };
|
|
27
33
|
const group = groups.get(reaction.emoji);
|
|
28
34
|
if (group) {
|
|
29
35
|
group.count++;
|
|
@@ -87,9 +93,11 @@ function CommentReactions({ comment, currentUserId, resolveName }) {
|
|
|
87
93
|
const renderReaction = useReactionRenderer();
|
|
88
94
|
const { components } = useCommentingOptions();
|
|
89
95
|
const reactions = useCommentReactions(editor, comment.id);
|
|
96
|
+
const msg = useTranslation();
|
|
97
|
+
const unknownAuthorName = msg(messages.commentsUnknownAuthor);
|
|
90
98
|
const summaries = useMemo(
|
|
91
|
-
() => summarizeReactions(reactions, currentUserId, resolveName),
|
|
92
|
-
[reactions, currentUserId, resolveName]
|
|
99
|
+
() => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),
|
|
100
|
+
[reactions, currentUserId, resolveName, unknownAuthorName]
|
|
93
101
|
);
|
|
94
102
|
const anyMenuOpen = useValue("any menu open", () => editor.menus.getOpenMenus().length > 0, [
|
|
95
103
|
editor
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-reactions.tsx"],
|
|
4
|
-
"sourcesContent": ["import { useMemo } from 'react'\nimport {\n\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { useMemo } from 'react'\nimport {\n\tcreateCommentReaction,\n\tcreateCommentReactionId,\n\tdefineMessages,\n\tEditor,\n\tTLComment,\n\tTLCommentReaction,\n\tuseEditor,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { RenderReaction } from '../ui/reaction'\nimport { ReactionPicker } from '../ui/reaction-picker'\nimport { Reactions, ReactionSummary } from '../ui/reactions'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { getCommentReactions } from './comment-store'\nimport { getCommentingOptions, useCommentingOptions } from './options'\n\nconst messages = defineMessages({\n\tcommentsUnknownAuthor: { id: 'comments.unknown-author', defaultMessage: 'Someone' },\n})\n\n/**\n * One comment's reactions, oldest first, reactively.\n *\n * @public\n */\nexport function useCommentReactions(\n\teditor: Editor,\n\tcommentId: TLComment['id']\n): TLCommentReaction[] {\n\treturn useValue(\n\t\t'comment reactions',\n\t\t() =>\n\t\t\tgetCommentReactions(editor)\n\t\t\t\t.filter((reaction) => reaction.commentId === commentId)\n\t\t\t\t.sort((a, b) => a.createdAt - b.createdAt),\n\t\t[editor, commentId]\n\t)\n}\n\n/**\n * The reaction fields {@link summarizeReactions} needs \u2014 a structural subset of\n * {@link tldraw#TLCommentReaction}, so tallies can also be built from rows synced outside the\n * editor store.\n *\n * @public\n */\nexport interface ReactionSummaryInput {\n\tuserId: string\n\temoji: string\n\tcreatedAt: number\n}\n\n/**\n * Tally a comment's reactions into an entry per emoji, ordered by when that emoji was first used so\n * the row stays stable as later reactions arrive. `active` marks the current user's emoji and\n * `reactors` lists who reacted, in reaction order. `resolveName` names each reactor; an id it can't\n * name falls back to `unknownAuthorName`, never the raw user id. Pass a translated name for that:\n * this is a plain function, so it can't reach the translations itself.\n *\n * @public\n */\nexport function summarizeReactions(\n\treactions: readonly ReactionSummaryInput[],\n\tcurrentUserId?: string | null,\n\tresolveName?: (userId: string) => string | undefined,\n\topts?: { unknownAuthorName?: string }\n): ReactionSummary[] {\n\tconst unknownAuthorName = opts?.unknownAuthorName ?? UNKNOWN_AUTHOR\n\tconst groups = new Map<\n\t\tstring,\n\t\t{ count: number; active: boolean; firstAt: number; reactors: ReactionSummary['reactors'] }\n\t>()\n\tfor (const reaction of reactions) {\n\t\tconst mine = currentUserId != null && reaction.userId === currentUserId\n\t\t// Fall back to a generic name, never the raw user id, when the id can't be resolved.\n\t\tconst reactor = { name: resolveName?.(reaction.userId) ?? unknownAuthorName, you: mine }\n\t\tconst group = groups.get(reaction.emoji)\n\t\tif (group) {\n\t\t\tgroup.count++\n\t\t\tgroup.active ||= mine\n\t\t\tgroup.firstAt = Math.min(group.firstAt, reaction.createdAt)\n\t\t\tgroup.reactors.push(reactor)\n\t\t} else {\n\t\t\tgroups.set(reaction.emoji, {\n\t\t\t\tcount: 1,\n\t\t\t\tactive: mine,\n\t\t\t\tfirstAt: reaction.createdAt,\n\t\t\t\treactors: [reactor],\n\t\t\t})\n\t\t}\n\t}\n\treturn [...groups]\n\t\t.sort(([, a], [, b]) => a.firstAt - b.firstAt)\n\t\t.map(([emoji, group]) => ({\n\t\t\temoji,\n\t\t\tcount: group.count,\n\t\t\tactive: group.active,\n\t\t\treactors: group.reactors,\n\t\t}))\n}\n\n/**\n * Toggle one user's reaction with a given emoji on a comment.\n *\n * Each reaction is its own record keyed by (comment, user, emoji), so this only touches that user's\n * own records and two people reacting at once never conflict. Behaviour depends on\n * `allowMultipleReactions`:\n *\n * - **multiple** (default): the emoji toggles independently, leaving other reactions alone.\n * - **single**: a new emoji replaces the user's existing reaction; the same one removes it.\n *\n * @public\n */\nexport function toggleCommentReaction(\n\teditor: Editor,\n\tcomment: TLComment,\n\tuserId: string,\n\temoji: string,\n\tnow = Date.now()\n): void {\n\tconst { allowMultipleReactions, isAllowedReaction } = getCommentingOptions(editor)\n\tconst targetId = createCommentReactionId(comment.id, userId, emoji)\n\tconst mine = getCommentReactions(editor).filter(\n\t\t(reaction) => reaction.commentId === comment.id && reaction.userId === userId\n\t)\n\tconst removing = mine.some((reaction) => reaction.id === targetId)\n\t// Only a token the configured palette allows may be added. Removals always go through \u2014 if a\n\t// reaction somehow carries an off-palette token, the user must still be able to clear it.\n\tif (!removing && !isAllowedReaction(emoji)) return\n\tcommitCommentMutation(editor, ({ put, remove }) => {\n\t\tif (removing) {\n\t\t\tremove([targetId])\n\t\t\treturn\n\t\t}\n\t\t// Single-select: a new emoji replaces the user's existing reaction(s) on this comment.\n\t\tif (!allowMultipleReactions && mine.length > 0) {\n\t\t\tremove(mine.map((reaction) => reaction.id))\n\t\t}\n\t\tput([\n\t\t\tcreateCommentReaction({\n\t\t\t\tcommentId: comment.id,\n\t\t\t\tthreadId: comment.threadId,\n\t\t\t\tpageId: comment.pageId,\n\t\t\t\tuserId,\n\t\t\t\temoji,\n\t\t\t\tnow,\n\t\t\t}),\n\t\t])\n\t})\n}\n\n/** @public */\nexport interface CommentReactionsProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted gives a read-only row (signed out): counts show, but the\n\t * pills don't toggle. */\n\tcurrentUserId?: string | null\n\t/** Names a reactor id for the hover list. Ids it can't name fall back to a generic \"Someone\". */\n\tresolveName?(userId: string): string | undefined\n}\n\n/**\n * Adapts the `ReactionContent` override into a `renderReaction` function for the presentational\n * components. Undefined when no override is set, so they fall back to drawing the token string.\n */\nfunction useReactionRenderer(): RenderReaction | undefined {\n\tconst { components } = useCommentingOptions()\n\tconst ReactionContent = components.ReactionContent\n\treturn useMemo(\n\t\t() => (ReactionContent ? (token: string) => <ReactionContent token={token} /> : undefined),\n\t\t[ReactionContent]\n\t)\n}\n\n/**\n * The tallied reaction row under one comment. Pair with `CommentReactionPicker`, which is what\n * adds a reaction.\n * @public @react\n */\nexport function CommentReactions({ comment, currentUserId, resolveName }: CommentReactionsProps) {\n\tconst editor = useEditor()\n\tconst renderReaction = useReactionRenderer()\n\tconst { components } = useCommentingOptions()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst msg = useTranslation()\n\tconst unknownAuthorName = msg(messages.commentsUnknownAuthor)\n\tconst summaries = useMemo(\n\t\t() => summarizeReactions(reactions, currentUserId, resolveName, { unknownAuthorName }),\n\t\t[reactions, currentUserId, resolveName, unknownAuthorName]\n\t)\n\t// Suppress the hover list while any menu is open (the reaction picker, an overflow menu\u2026) so it\n\t// doesn't compete with the menu the user is actually working in. Edit mode already hides the\n\t// pills entirely (the card becomes a composer), so it needs no special case here.\n\tconst anyMenuOpen = useValue('any menu open', () => editor.menus.getOpenMenus().length > 0, [\n\t\teditor,\n\t])\n\treturn (\n\t\t<Reactions\n\t\t\treactions={summaries}\n\t\t\tcanReact={currentUserId != null}\n\t\t\tenableHoverList={!anyMenuOpen}\n\t\t\trenderReaction={renderReaction}\n\t\t\tReactionTooltip={components.ReactionTooltip}\n\t\t\tonToggle={(value) => {\n\t\t\t\tif (currentUserId == null) return\n\t\t\t\ttoggleCommentReaction(editor, comment, currentUserId, value)\n\t\t\t}}\n\t\t/>\n\t)\n}\n\n/** @public */\nexport interface CommentReactionPickerProps {\n\tcomment: TLComment\n\t/** The reacting user. Null/omitted renders nothing \u2014 there's nobody to react as. */\n\tcurrentUserId?: string | null\n\t/** The emoji the picker offers. Defaults to `DEFAULT_REACTION_EMOJI`. */\n\temoji?: string[]\n}\n\n/**\n * The add-reaction button for one comment. Belongs with the comment card's hover actions rather\n * than in the reaction row, so opening it doesn't chase the row as reactions are added.\n * @public @react\n */\nexport function CommentReactionPicker({\n\tcomment,\n\tcurrentUserId,\n\temoji,\n}: CommentReactionPickerProps) {\n\tconst editor = useEditor()\n\tconst { components } = useCommentingOptions()\n\tconst renderReaction = useReactionRenderer()\n\tconst reactions = useCommentReactions(editor, comment.id)\n\tconst selected = useMemo(\n\t\t() =>\n\t\t\treactions\n\t\t\t\t.filter((reaction) => currentUserId != null && reaction.userId === currentUserId)\n\t\t\t\t.map((reaction) => reaction.emoji),\n\t\t[reactions, currentUserId]\n\t)\n\tif (currentUserId == null) return null\n\treturn (\n\t\t<ReactionPicker\n\t\t\temoji={emoji}\n\t\t\tselected={selected}\n\t\t\trenderReaction={renderReaction}\n\t\t\tpalette={components.ReactionPalette}\n\t\t\tmenuId={`comment-reactions-${comment.id}`}\n\t\t\tonSelect={(value) => toggleCommentReaction(editor, comment, currentUserId, value)}\n\t\t/>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-render.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Extensions, generateText, JSONContent } from '@tiptap/core'\nimport { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'\nimport { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'\nimport { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'\n\n/**\n * The author name
|
|
5
|
-
"mappings": "AAAA,SAA0B,oBAAiC;AAC3D,SAA6B,8BAA8B;AAC3D,SAAS,4CAAwD;AACjE,SAAS,yBAAyB,sBAAsB;
|
|
4
|
+
"sourcesContent": ["import { type Extensions, generateText, JSONContent } from '@tiptap/core'\nimport { type CommentAuthor, createMentionExtension } from '@tldraw/mentions'\nimport { renderHtmlFromRichTextWithExtensions, TLRichText } from 'tldraw'\nimport { commentTipTapExtensions, isCommentEmpty } from '../ui/comment-extensions'\n\n/**\n * The author name used when no source can name an id (e.g. a deleted account, or a member with no\n * comment and no live presence). Hosts pre-empt it by resolving the id from `resolveAuthor`.\n *\n * The English fallback, for the places that can't translate: it's a module constant, so the UI\n * reads `comments.unknown-author` instead and only lands here when there are no translations at\n * all. Pass a translated name to {@link summarizeReactions} for the same reason.\n */\nexport const UNKNOWN_AUTHOR = 'Someone'\n\n/** The `CommentAuthor` fallback for an unresolvable id \u2014 just the name, no color or image.\n * Frozen: the one shared instance is handed to host render slots. */\nexport const UNKNOWN_COMMENT_AUTHOR: CommentAuthor = Object.freeze({ name: UNKNOWN_AUTHOR })\n\n/**\n * The comment extension set has no heading node, so a body that nonetheless contains one (e.g. a\n * record created programmatically or synced from a client with a fuller set) would make TipTap\n * throw on an unknown node type. Demote any heading to a paragraph first, so headings can never\n * render as headings and rendering never crashes.\n */\nfunction demoteHeadings(node: JSONContent): JSONContent {\n\tconst content = Array.isArray(node.content) ? node.content.map(demoteHeadings) : node.content\n\tif (node.type === 'heading') {\n\t\tconst { attrs: _attrs, ...rest } = node\n\t\treturn { ...rest, type: 'paragraph', content }\n\t}\n\treturn content === node.content ? node : { ...node, content }\n}\n\n/**\n * Whether a body contains a mention node. Mention text is resolved from a member id at render time,\n * so a body with one can't be cached by identity alone (the same body renders differently as names\n * change) \u2014 those bodies skip the cache and re-render each call.\n */\nfunction hasMention(node: JSONContent): boolean {\n\tif (node.type === 'mention') return true\n\treturn Array.isArray(node.content) ? node.content.some(hasMention) : false\n}\n\nconst htmlCache = new WeakMap<TLRichText, string>()\nconst textCache = new WeakMap<TLRichText, string>()\n\n/**\n * Render a body through the comment extension set, memoized by body identity in `cache` unless the\n * body carries a mention (see {@link hasMention}), in which case the mention extension resolving\n * `resolveName` is added and nothing is cached.\n */\nfunction renderWithCommentExtensions(\n\tcache: WeakMap<TLRichText, string>,\n\trichText: TLRichText,\n\tresolveName: ((id: string) => string | undefined) | undefined,\n\trender: (doc: JSONContent, extensions: Extensions) => string\n): string {\n\tconst mentions = hasMention(richText as JSONContent)\n\tif (!mentions) {\n\t\tconst cached = cache.get(richText)\n\t\tif (cached !== undefined) return cached\n\t}\n\tconst extensions = mentions\n\t\t? [...commentTipTapExtensions, createMentionExtension({ resolveName })]\n\t\t: commentTipTapExtensions\n\tconst result = render(demoteHeadings(richText as JSONContent), extensions)\n\tif (!mentions) cache.set(richText, result)\n\treturn result\n}\n\n/**\n * Render a comment body to HTML through the limited comment extension set (no headings), so a body\n * always renders with comment formatting regardless of the host editor's rich-text config. Renders\n * through tldraw's shared helper, so it gets the same empty-paragraph fix that keeps blank lines\n * from collapsing. `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentHtml(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\treturn renderWithCommentExtensions(htmlCache, richText, resolveName, (doc, extensions) =>\n\t\trenderHtmlFromRichTextWithExtensions(doc as TLRichText, extensions)\n\t)\n}\n\n/**\n * Flatten a comment body to plaintext through the limited comment extension set \u2014 paragraphs and\n * list items separated by newlines. Used for previews (e.g. the sidebar) where formatting is dropped.\n * `resolveName` maps a member id to its current name for any @mentions.\n */\nexport function renderCommentPlaintext(\n\trichText: TLRichText,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tif (isCommentEmpty(richText)) return ''\n\treturn renderWithCommentExtensions(textCache, richText, resolveName, (doc, extensions) =>\n\t\tgenerateText(doc, extensions, { blockSeparator: '\\n' })\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAA0B,oBAAiC;AAC3D,SAA6B,8BAA8B;AAC3D,SAAS,4CAAwD;AACjE,SAAS,yBAAyB,sBAAsB;AAUjD,MAAM,iBAAiB;AAIvB,MAAM,yBAAwC,OAAO,OAAO,EAAE,MAAM,eAAe,CAAC;AAQ3F,SAAS,eAAe,MAAgC;AACvD,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,cAAc,IAAI,KAAK;AACtF,MAAI,KAAK,SAAS,WAAW;AAC5B,UAAM,EAAE,OAAO,QAAQ,GAAG,KAAK,IAAI;AACnC,WAAO,EAAE,GAAG,MAAM,MAAM,aAAa,QAAQ;AAAA,EAC9C;AACA,SAAO,YAAY,KAAK,UAAU,OAAO,EAAE,GAAG,MAAM,QAAQ;AAC7D;AAOA,SAAS,WAAW,MAA4B;AAC/C,MAAI,KAAK,SAAS,UAAW,QAAO;AACpC,SAAO,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,KAAK,UAAU,IAAI;AACtE;AAEA,MAAM,YAAY,oBAAI,QAA4B;AAClD,MAAM,YAAY,oBAAI,QAA4B;AAOlD,SAAS,4BACR,OACA,UACA,aACA,QACS;AACT,QAAM,WAAW,WAAW,QAAuB;AACnD,MAAI,CAAC,UAAU;AACd,UAAM,SAAS,MAAM,IAAI,QAAQ;AACjC,QAAI,WAAW,OAAW,QAAO;AAAA,EAClC;AACA,QAAM,aAAa,WAChB,CAAC,GAAG,yBAAyB,uBAAuB,EAAE,YAAY,CAAC,CAAC,IACpE;AACH,QAAM,SAAS,OAAO,eAAe,QAAuB,GAAG,UAAU;AACzE,MAAI,CAAC,SAAU,OAAM,IAAI,UAAU,MAAM;AACzC,SAAO;AACR;AAQO,SAAS,kBACf,UACA,aACS;AACT,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,qCAAqC,KAAmB,UAAU;AAAA,EACnE;AACD;AAOO,SAAS,uBACf,UACA,aACS;AACT,MAAI,eAAe,QAAQ,EAAG,QAAO;AACrC,SAAO;AAAA,IAA4B;AAAA,IAAW;AAAA,IAAU;AAAA,IAAa,CAAC,KAAK,eAC1E,aAAa,KAAK,YAAY,EAAE,gBAAgB,KAAK,CAAC;AAAA,EACvD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
StateNode
|
|
2
|
+
StateNode,
|
|
3
|
+
defineMessages
|
|
3
4
|
} from "tldraw";
|
|
4
5
|
import { defaultCommentingOptions, getCommentingOptions } from "./options.mjs";
|
|
5
6
|
import { commentsSidebarOpen, pendingComment, regionDraft } from "./state.mjs";
|
|
@@ -189,14 +190,20 @@ class CommentDragging extends StateNode {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
const commentTools = [CommentTool];
|
|
193
|
+
const messages = defineMessages({
|
|
194
|
+
toolComment: { id: "tool.comment", defaultMessage: "Comment" }
|
|
195
|
+
});
|
|
192
196
|
const commentToolOverrides = {
|
|
193
197
|
tools(editor, tools) {
|
|
194
198
|
tools.comment = {
|
|
195
199
|
id: "comment",
|
|
196
200
|
icon: "comment",
|
|
197
|
-
label:
|
|
201
|
+
label: messages.toolComment.id,
|
|
198
202
|
kbd: "c",
|
|
199
|
-
onSelect: () =>
|
|
203
|
+
onSelect: (source) => {
|
|
204
|
+
if (source === "context-menu") startCommentAt(editor, editor.inputs.getOriginPagePoint());
|
|
205
|
+
else editor.setCurrentTool("comment");
|
|
206
|
+
}
|
|
200
207
|
};
|
|
201
208
|
return tools;
|
|
202
209
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-tool.tsx"],
|
|
4
|
-
"sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tStateNode,\n\tTLCommentAnchor,\n\tTLStateNodeConstructor,\n\tTLUiOverrides,\n\tVecLike,\n} from 'tldraw'\nimport { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'\nimport { commentsSidebarOpen, pendingComment, regionDraft } from './state'\nimport { commentTargetShapeAt, regionPinPoint, shapeAnchorAt } from './thread-state'\n\n/** A comment being placed but not yet posted: where its composer sits and what it will anchor\n * to. Shared between the tool (which sets it on click) and the overlay (which renders the\n * composer) through the internal `pendingComment` atom.\n * @internal */\nexport interface PendingComment {\n\tanchor: TLCommentAnchor\n\t/** Page point where the composer opens (the click location, or a region's pin corner). */\n\tpoint: VecLike\n}\n\n/**\n * Merge configure options over a base. Shallow for scalars; `components` is merged rather than\n * replaced so chained `configure` calls layer their slots \u2014 a later `{ components: { PinContent } }`\n * doesn't drop an earlier `{ components: { CommentBody } }`.\n */\nfunction mergeCommentingOptions(\n\tbase: CommentingOptions,\n\toverrides: Partial<CommentingOptions>\n): CommentingOptions {\n\treturn {\n\t\t...base,\n\t\t...overrides,\n\t\tcomponents: { ...base.components, ...overrides.components },\n\t}\n}\n\n/** The page-space rectangle spanned by two points, normalized so w/h are non-negative. Shared by\n * region creation (pointer-down \u2192 cursor) and corner resize (fixed corner \u2192 cursor). */\nexport function regionBetween(a: VecLike, b: VecLike): BoxModel {\n\treturn {\n\t\tx: Math.min(a.x, b.x),\n\t\ty: Math.min(a.y, b.y),\n\t\tw: Math.abs(a.x - b.x),\n\t\th: Math.abs(a.y - b.y),\n\t}\n}\n\n/**\n * The comment tool. Pressing down opens the comment composer at the pointer and it follows until\n * release \u2014 like placing a sticky note \u2014 settling on a point, or on a shape when released over one.\n * With region comments enabled, dragging past the threshold draws a region rectangle instead.\n * Placement only opens a composer; the records are created when the comment is posted. The tool\n * stays active while the composer is open \u2014 posting returns to select, and so does clicking the\n * canvas elsewhere, which cancels the placement.\n * @public\n */\nexport class CommentTool extends StateNode {\n\tstatic override id = 'comment'\n\tstatic override initial = 'idle'\n\tstatic override children(): TLStateNodeConstructor[] {\n\t\treturn [CommentIdle, CommentPointing, CommentDragging]\n\t}\n\n\t/**\n\t * Configure this tool's {@link CommentTool.options | `options`}, returning a configured subclass\n\t * to register via `tools`. Mirrors `ShapeUtil.configure`. Layers over any prior `configure`, so\n\t * calls can be chained.\n\t *\n\t * @example\n\t * ```tsx\n\t * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n\t * ```\n\t */\n\tstatic configure<T extends TLStateNodeConstructor>(\n\t\tthis: T,\n\t\toptions: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never\n\t): T {\n\t\t// @ts-expect-error -- mirrors ShapeUtil.configure; extending `this` is sound at runtime.\n\t\treturn class extends this {\n\t\t\t// @ts-expect-error\n\t\t\toptions = mergeCommentingOptions(this.options, options)\n\t\t}\n\t}\n\n\t/**\n\t * The merged commenting options for this editor. Read from anywhere via\n\t * {@link getCommentingOptions}. Override with {@link CommentTool.configure}.\n\t */\n\toptions: CommentingOptions = defaultCommentingOptions\n\n\toverride onEnter() {\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// Placing comments is canvas-focused \u2014 the thread list gets out of the way while the tool is\n\t\t// active. Reopened via its own control (a button), never by leaving the tool.\n\t\tcommentsSidebarOpen.set(this.editor, false)\n\t}\n\n\toverride onExit() {\n\t\t// Drop the hover hint painted while pointing at shapes (see CommentIdle). The cursor resets\n\t\t// when the next tool takes over. The draft composer and region draft belong to the tool, so\n\t\t// they leave with it; the draft's text survives in the comment draft store.\n\t\tthis.editor.setHintingShapes([])\n\t\tpendingComment.set(this.editor, null)\n\t\tregionDraft.set(this.editor, null)\n\t}\n\n\t// Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).\n\toverride onCancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nfunction pendingCommentAt(editor: Editor, point: VecLike): PendingComment {\n\tconst hit = commentTargetShapeAt(editor, point)\n\tconst anchor: TLCommentAnchor = hit\n\t\t? shapeAnchorAt(\n\t\t\t\teditor,\n\t\t\t\thit.id,\n\t\t\t\tpoint,\n\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\tpoint,\n\t\t\t\t\taltKey: editor.inputs.getAltKey(),\n\t\t\t\t})\n\t\t\t)\n\t\t: { type: 'point', x: point.x, y: point.y }\n\treturn { anchor, point: { x: point.x, y: point.y } }\n}\n\n/**\n * Open the comment composer at a page point, anchored as a comment tool click there would be. Use\n * it to start a comment from outside the tool, such as a context menu item. Enters the comment tool\n * (which must be registered), since the draft composer closes when the tool exits. It doesn't check\n * `canComment`: a viewer who can't comment sees the `ComposerFallback` slot instead.\n *\n * @example\n * ```tsx\n * <TldrawUiMenuItem id=\"comment\" label=\"Comment\" onSelect={() => startCommentAt(editor, point)} />\n * ```\n * @public\n */\nexport function startCommentAt(editor: Editor, point: VecLike) {\n\teditor.setCurrentTool('comment')\n\tconst pending = pendingCommentAt(editor, point)\n\tpendingComment.set(editor, pending)\n\t// Entering the tool hints the shape under the live pointer, which is wherever the caller's UI\n\t// was (a menu item), not `point`.\n\teditor.setHintingShapes(pending.anchor.type === 'shape' ? [pending.anchor.shapeId] : [])\n}\n\n/** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the\n * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so\n * this shows the select-style outline while the comment tool \u2014 not select \u2014 is active. */\nfunction updateAnchorHint(editor: Editor) {\n\tconst hit = commentTargetShapeAt(editor, editor.inputs.getCurrentPagePoint())\n\teditor.setHintingShapes(hit ? [hit.id] : [])\n}\n\nclass CommentIdle extends StateNode {\n\tstatic override id = 'idle'\n\n\toverride onEnter() {\n\t\t// Back to hovering: restore the pin cursor (a prior placing state may have hidden it).\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// With a region composer open, a shape outline under its pin corner would imply a shape\n\t\t// anchor the region will never use \u2014 and it would stick, since moves over the composer\n\t\t// never reach the canvas. Point placements keep the hint: it previews the next click.\n\t\tif (pendingComment.get(this.editor)?.anchor.type === 'region') {\n\t\t\tthis.editor.setHintingShapes([])\n\t\t} else {\n\t\t\tupdateAnchorHint(this.editor)\n\t\t}\n\t}\n\n\toverride onPointerMove() {\n\t\tupdateAnchorHint(this.editor)\n\t}\n\n\toverride onPointerDown() {\n\t\t// A press away from an open composer cancels the placement rather than moving it. The\n\t\t// tool's onExit drops the draft composer; its text survives in the draft store.\n\t\tif (pendingComment.get(this.editor)) {\n\t\t\tthis.editor.setCurrentTool('select')\n\t\t\treturn\n\t\t}\n\t\tthis.parent.transition('pointing')\n\t}\n}\n\nclass CommentPointing extends StateNode {\n\tstatic override id = 'pointing'\n\n\toverride onEnter() {\n\t\t// Open the composer immediately at the press point so it's visible from pointer-down (not just\n\t\t// on release), and let it trail the pointer while dragging \u2014 like placing a sticky note. The\n\t\t// anchor is a bare point for now; it's resolved (shape or point) on pointer up.\n\t\tconst { editor } = this\n\t\t// Hide the cursor while placing: the draft composer is the pointer's stand-in now, so the pin\n\t\t// cursor sitting over it just reads as clutter.\n\t\teditor.setCursor({ type: 'none', rotation: 0 })\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'point', x: point.x, y: point.y },\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\t}\n\n\toverride onPointerMove() {\n\t\tconst { editor } = this\n\t\t// Once the pointer passes the drag threshold with region comments enabled, this is a region,\n\t\t// not a follow \u2014 hand off to the region drag (which clears this composer and draws the box).\n\t\tif (getCommentingOptions(editor).enableRegions && editor.inputs.getIsDragging()) {\n\t\t\tthis.parent.transition('dragging')\n\t\t\treturn\n\t\t}\n\t\t// Otherwise the composer trails the pointer \u2014 keep hinting the shape a release here would\n\t\t// anchor to, like the idle hover does.\n\t\tupdateAnchorHint(editor)\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.update(editor, (p) => (p ? { ...p, point: { x: point.x, y: point.y } } : p))\n\t}\n\n\t// Settle where the pointer is released: anchor to the shape under it, or drop a point.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, pendingCommentAt(editor, point))\n\t\t// Stay in the tool while the composer is open \u2014 the interaction isn't over until the\n\t\t// comment is posted or dismissed, and staying keeps the surrounding UI (style panel,\n\t\t// sidebar) from churning mid-placement.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\t// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.). The\n\t// tool's onExit drops the draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nclass CommentDragging extends StateNode {\n\tstatic override id = 'dragging'\n\n\toverride onEnter() {\n\t\t// A region drag supersedes the point-follow composer opened in `pointing`. Show a crosshair again,\n\t\t// since the composer no longer stands in for the pointer, and drop the placement hint \u2014 a region\n\t\t// anchors to its rectangle, so a single-shape outline would be stale.\n\t\tpendingComment.set(this.editor, null)\n\t\tthis.editor.setHintingShapes([])\n\t\tthis.editor.setCursor({ type: 'cross', rotation: 0 })\n\t\tthis.updateDraft()\n\t}\n\n\toverride onPointerMove() {\n\t\tthis.updateDraft()\n\t}\n\n\t// Commit the dragged rectangle as a region anchor; the composer opens at its pin corner.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst origin = editor.inputs.getOriginPagePoint()\n\t\tconst current = editor.inputs.getCurrentPagePoint()\n\t\tconst region = regionBetween(origin, current)\n\t\t// The pin lives on the corner the drag released on \u2014 drag up-left, pin top-left.\n\t\tconst pin = { x: current.x >= origin.x ? 1 : 0, y: current.y >= origin.y ? 1 : 0 }\n\t\tregionDraft.set(editor, null)\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'region', ...region, pinX: pin.x, pinY: pin.y },\n\t\t\tpoint: regionPinPoint(region, pin),\n\t\t})\n\t\t// Same as the point placement: the tool stays active while the composer is open.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\tprivate updateDraft() {\n\t\tconst { editor } = this\n\t\tregionDraft.set(\n\t\t\teditor,\n\t\t\tregionBetween(editor.inputs.getOriginPagePoint(), editor.inputs.getCurrentPagePoint())\n\t\t)\n\t}\n\n\t// The tool's onExit drops the region draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** @public */\nexport const commentTools = [CommentTool]\n\n/** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.\n * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button.\n * @public */\nexport const commentToolOverrides: TLUiOverrides = {\n\ttools(editor, tools) {\n\t\ttools.comment = {\n\t\t\tid: 'comment',\n\t\t\ticon: 'comment',\n\t\t\tlabel: 'Comment',\n\t\t\tkbd: 'c',\n\t\t\tonSelect: () => editor.setCurrentTool('comment'),\n\t\t}\n\t\treturn tools\n\t},\n}\n"],
|
|
5
|
-
"mappings": "AAAA;AAAA,EAGC;AAAA,
|
|
4
|
+
"sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tStateNode,\n\tTLCommentAnchor,\n\tTLStateNodeConstructor,\n\tTLUiOverrides,\n\tVecLike,\n\tdefineMessages,\n} from 'tldraw'\nimport { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'\nimport { commentsSidebarOpen, pendingComment, regionDraft } from './state'\nimport { commentTargetShapeAt, regionPinPoint, shapeAnchorAt } from './thread-state'\n\n/** A comment being placed but not yet posted: where its composer sits and what it will anchor\n * to. Shared between the tool (which sets it on click) and the overlay (which renders the\n * composer) through the internal `pendingComment` atom.\n * @internal */\nexport interface PendingComment {\n\tanchor: TLCommentAnchor\n\t/** Page point where the composer opens (the click location, or a region's pin corner). */\n\tpoint: VecLike\n}\n\n/**\n * Merge configure options over a base. Shallow for scalars; `components` is merged rather than\n * replaced so chained `configure` calls layer their slots \u2014 a later `{ components: { PinContent } }`\n * doesn't drop an earlier `{ components: { CommentBody } }`.\n */\nfunction mergeCommentingOptions(\n\tbase: CommentingOptions,\n\toverrides: Partial<CommentingOptions>\n): CommentingOptions {\n\treturn {\n\t\t...base,\n\t\t...overrides,\n\t\tcomponents: { ...base.components, ...overrides.components },\n\t}\n}\n\n/** The page-space rectangle spanned by two points, normalized so w/h are non-negative. Shared by\n * region creation (pointer-down \u2192 cursor) and corner resize (fixed corner \u2192 cursor). */\nexport function regionBetween(a: VecLike, b: VecLike): BoxModel {\n\treturn {\n\t\tx: Math.min(a.x, b.x),\n\t\ty: Math.min(a.y, b.y),\n\t\tw: Math.abs(a.x - b.x),\n\t\th: Math.abs(a.y - b.y),\n\t}\n}\n\n/**\n * The comment tool. Pressing down opens the comment composer at the pointer and it follows until\n * release \u2014 like placing a sticky note \u2014 settling on a point, or on a shape when released over one.\n * With region comments enabled, dragging past the threshold draws a region rectangle instead.\n * Placement only opens a composer; the records are created when the comment is posted. The tool\n * stays active while the composer is open \u2014 posting returns to select, and so does clicking the\n * canvas elsewhere, which cancels the placement.\n * @public\n */\nexport class CommentTool extends StateNode {\n\tstatic override id = 'comment'\n\tstatic override initial = 'idle'\n\tstatic override children(): TLStateNodeConstructor[] {\n\t\treturn [CommentIdle, CommentPointing, CommentDragging]\n\t}\n\n\t/**\n\t * Configure this tool's {@link CommentTool.options | `options`}, returning a configured subclass\n\t * to register via `tools`. Mirrors `ShapeUtil.configure`. Layers over any prior `configure`, so\n\t * calls can be chained.\n\t *\n\t * @example\n\t * ```tsx\n\t * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n\t * ```\n\t */\n\tstatic configure<T extends TLStateNodeConstructor>(\n\t\tthis: T,\n\t\toptions: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never\n\t): T {\n\t\t// @ts-expect-error -- mirrors ShapeUtil.configure; extending `this` is sound at runtime.\n\t\treturn class extends this {\n\t\t\t// @ts-expect-error\n\t\t\toptions = mergeCommentingOptions(this.options, options)\n\t\t}\n\t}\n\n\t/**\n\t * The merged commenting options for this editor. Read from anywhere via\n\t * {@link getCommentingOptions}. Override with {@link CommentTool.configure}.\n\t */\n\toptions: CommentingOptions = defaultCommentingOptions\n\n\toverride onEnter() {\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// Placing comments is canvas-focused \u2014 the thread list gets out of the way while the tool is\n\t\t// active. Reopened via its own control (a button), never by leaving the tool.\n\t\tcommentsSidebarOpen.set(this.editor, false)\n\t}\n\n\toverride onExit() {\n\t\t// Drop the hover hint painted while pointing at shapes (see CommentIdle). The cursor resets\n\t\t// when the next tool takes over. The draft composer and region draft belong to the tool, so\n\t\t// they leave with it; the draft's text survives in the comment draft store.\n\t\tthis.editor.setHintingShapes([])\n\t\tpendingComment.set(this.editor, null)\n\t\tregionDraft.set(this.editor, null)\n\t}\n\n\t// Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).\n\toverride onCancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nfunction pendingCommentAt(editor: Editor, point: VecLike): PendingComment {\n\tconst hit = commentTargetShapeAt(editor, point)\n\tconst anchor: TLCommentAnchor = hit\n\t\t? shapeAnchorAt(\n\t\t\t\teditor,\n\t\t\t\thit.id,\n\t\t\t\tpoint,\n\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\tpoint,\n\t\t\t\t\taltKey: editor.inputs.getAltKey(),\n\t\t\t\t})\n\t\t\t)\n\t\t: { type: 'point', x: point.x, y: point.y }\n\treturn { anchor, point: { x: point.x, y: point.y } }\n}\n\n/**\n * Open the comment composer at a page point, anchored as a comment tool click there would be. Use\n * it to start a comment from outside the tool, such as a context menu item. Enters the comment tool\n * (which must be registered), since the draft composer closes when the tool exits. It doesn't check\n * `canComment`: a viewer who can't comment sees the `ComposerFallback` slot instead.\n *\n * @example\n * ```tsx\n * <TldrawUiMenuItem id=\"comment\" label=\"Comment\" onSelect={() => startCommentAt(editor, point)} />\n * ```\n * @public\n */\nexport function startCommentAt(editor: Editor, point: VecLike) {\n\teditor.setCurrentTool('comment')\n\tconst pending = pendingCommentAt(editor, point)\n\tpendingComment.set(editor, pending)\n\t// Entering the tool hints the shape under the live pointer, which is wherever the caller's UI\n\t// was (a menu item), not `point`.\n\teditor.setHintingShapes(pending.anchor.type === 'shape' ? [pending.anchor.shapeId] : [])\n}\n\n/** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the\n * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so\n * this shows the select-style outline while the comment tool \u2014 not select \u2014 is active. */\nfunction updateAnchorHint(editor: Editor) {\n\tconst hit = commentTargetShapeAt(editor, editor.inputs.getCurrentPagePoint())\n\teditor.setHintingShapes(hit ? [hit.id] : [])\n}\n\nclass CommentIdle extends StateNode {\n\tstatic override id = 'idle'\n\n\toverride onEnter() {\n\t\t// Back to hovering: restore the pin cursor (a prior placing state may have hidden it).\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// With a region composer open, a shape outline under its pin corner would imply a shape\n\t\t// anchor the region will never use \u2014 and it would stick, since moves over the composer\n\t\t// never reach the canvas. Point placements keep the hint: it previews the next click.\n\t\tif (pendingComment.get(this.editor)?.anchor.type === 'region') {\n\t\t\tthis.editor.setHintingShapes([])\n\t\t} else {\n\t\t\tupdateAnchorHint(this.editor)\n\t\t}\n\t}\n\n\toverride onPointerMove() {\n\t\tupdateAnchorHint(this.editor)\n\t}\n\n\toverride onPointerDown() {\n\t\t// A press away from an open composer cancels the placement rather than moving it. The\n\t\t// tool's onExit drops the draft composer; its text survives in the draft store.\n\t\tif (pendingComment.get(this.editor)) {\n\t\t\tthis.editor.setCurrentTool('select')\n\t\t\treturn\n\t\t}\n\t\tthis.parent.transition('pointing')\n\t}\n}\n\nclass CommentPointing extends StateNode {\n\tstatic override id = 'pointing'\n\n\toverride onEnter() {\n\t\t// Open the composer immediately at the press point so it's visible from pointer-down (not just\n\t\t// on release), and let it trail the pointer while dragging \u2014 like placing a sticky note. The\n\t\t// anchor is a bare point for now; it's resolved (shape or point) on pointer up.\n\t\tconst { editor } = this\n\t\t// Hide the cursor while placing: the draft composer is the pointer's stand-in now, so the pin\n\t\t// cursor sitting over it just reads as clutter.\n\t\teditor.setCursor({ type: 'none', rotation: 0 })\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'point', x: point.x, y: point.y },\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\t}\n\n\toverride onPointerMove() {\n\t\tconst { editor } = this\n\t\t// Once the pointer passes the drag threshold with region comments enabled, this is a region,\n\t\t// not a follow \u2014 hand off to the region drag (which clears this composer and draws the box).\n\t\tif (getCommentingOptions(editor).enableRegions && editor.inputs.getIsDragging()) {\n\t\t\tthis.parent.transition('dragging')\n\t\t\treturn\n\t\t}\n\t\t// Otherwise the composer trails the pointer \u2014 keep hinting the shape a release here would\n\t\t// anchor to, like the idle hover does.\n\t\tupdateAnchorHint(editor)\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.update(editor, (p) => (p ? { ...p, point: { x: point.x, y: point.y } } : p))\n\t}\n\n\t// Settle where the pointer is released: anchor to the shape under it, or drop a point.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, pendingCommentAt(editor, point))\n\t\t// Stay in the tool while the composer is open \u2014 the interaction isn't over until the\n\t\t// comment is posted or dismissed, and staying keeps the surrounding UI (style panel,\n\t\t// sidebar) from churning mid-placement.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\t// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.). The\n\t// tool's onExit drops the draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nclass CommentDragging extends StateNode {\n\tstatic override id = 'dragging'\n\n\toverride onEnter() {\n\t\t// A region drag supersedes the point-follow composer opened in `pointing`. Show a crosshair again,\n\t\t// since the composer no longer stands in for the pointer, and drop the placement hint \u2014 a region\n\t\t// anchors to its rectangle, so a single-shape outline would be stale.\n\t\tpendingComment.set(this.editor, null)\n\t\tthis.editor.setHintingShapes([])\n\t\tthis.editor.setCursor({ type: 'cross', rotation: 0 })\n\t\tthis.updateDraft()\n\t}\n\n\toverride onPointerMove() {\n\t\tthis.updateDraft()\n\t}\n\n\t// Commit the dragged rectangle as a region anchor; the composer opens at its pin corner.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst origin = editor.inputs.getOriginPagePoint()\n\t\tconst current = editor.inputs.getCurrentPagePoint()\n\t\tconst region = regionBetween(origin, current)\n\t\t// The pin lives on the corner the drag released on \u2014 drag up-left, pin top-left.\n\t\tconst pin = { x: current.x >= origin.x ? 1 : 0, y: current.y >= origin.y ? 1 : 0 }\n\t\tregionDraft.set(editor, null)\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'region', ...region, pinX: pin.x, pinY: pin.y },\n\t\t\tpoint: regionPinPoint(region, pin),\n\t\t})\n\t\t// Same as the point placement: the tool stays active while the composer is open.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\tprivate updateDraft() {\n\t\tconst { editor } = this\n\t\tregionDraft.set(\n\t\t\teditor,\n\t\t\tregionBetween(editor.inputs.getOriginPagePoint(), editor.inputs.getCurrentPagePoint())\n\t\t)\n\t}\n\n\t// The tool's onExit drops the region draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** @public */\nexport const commentTools = [CommentTool]\n\nconst messages = defineMessages({\n\ttoolComment: { id: 'tool.comment', defaultMessage: 'Comment' },\n})\n\n/** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.\n * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button, and its\n * `DefaultContextMenuContent` shows a comment item that opens the composer where the menu was\n * opened.\n * @public */\nexport const commentToolOverrides: TLUiOverrides = {\n\ttools(editor, tools) {\n\t\ttools.comment = {\n\t\t\tid: 'comment',\n\t\t\ticon: 'comment',\n\t\t\tlabel: messages.toolComment.id,\n\t\t\tkbd: 'c',\n\t\t\tonSelect: (source) => {\n\t\t\t\t// The menu was opened by the last pointer down; the live pointer is over the menu now.\n\t\t\t\tif (source === 'context-menu') startCommentAt(editor, editor.inputs.getOriginPagePoint())\n\t\t\t\telse editor.setCurrentTool('comment')\n\t\t\t},\n\t\t}\n\t\treturn tools\n\t},\n}\n"],
|
|
5
|
+
"mappings": "AAAA;AAAA,EAGC;AAAA,EAKA;AAAA,OACM;AACP,SAAiC,0BAA0B,4BAA4B;AACvF,SAAS,qBAAqB,gBAAgB,mBAAmB;AACjE,SAAS,sBAAsB,gBAAgB,qBAAqB;AAiBpE,SAAS,uBACR,MACA,WACoB;AACpB,SAAO;AAAA,IACN,GAAG;AAAA,IACH,GAAG;AAAA,IACH,YAAY,EAAE,GAAG,KAAK,YAAY,GAAG,UAAU,WAAW;AAAA,EAC3D;AACD;AAIO,SAAS,cAAc,GAAY,GAAsB;AAC/D,SAAO;AAAA,IACN,GAAG,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IACpB,GAAG,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IACpB,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,IACrB,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,EACtB;AACD;AAWO,MAAM,oBAAoB,UAAU;AAAA,EAC1C,OAAgB,KAAK;AAAA,EACrB,OAAgB,UAAU;AAAA,EAC1B,OAAgB,WAAqC;AACpD,WAAO,CAAC,aAAa,iBAAiB,eAAe;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,OAAO,UAEN,SACI;AAEJ,WAAO,cAAc,KAAK;AAAA;AAAA,MAEzB,UAAU,uBAAuB,KAAK,SAAS,OAAO;AAAA,IACvD;AAAA,EACD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAA6B;AAAA,EAEpB,UAAU;AAClB,SAAK,OAAO,UAAU,EAAE,MAAM,WAAW,UAAU,EAAE,CAAC;AAGtD,wBAAoB,IAAI,KAAK,QAAQ,KAAK;AAAA,EAC3C;AAAA,EAES,SAAS;AAIjB,SAAK,OAAO,iBAAiB,CAAC,CAAC;AAC/B,mBAAe,IAAI,KAAK,QAAQ,IAAI;AACpC,gBAAY,IAAI,KAAK,QAAQ,IAAI;AAAA,EAClC;AAAA;AAAA,EAGS,WAAW;AACnB,SAAK,OAAO,eAAe,QAAQ;AAAA,EACpC;AACD;AAEA,SAAS,iBAAiB,QAAgB,OAAgC;AACzE,QAAM,MAAM,qBAAqB,QAAQ,KAAK;AAC9C,QAAM,SAA0B,MAC7B;AAAA,IACA;AAAA,IACA,IAAI;AAAA,IACJ;AAAA,IACA,qBAAqB,MAAM,EAAE,gBAAgB,QAAQ;AAAA,MACpD,SAAS,IAAI;AAAA,MACb;AAAA,MACA,QAAQ,OAAO,OAAO,UAAU;AAAA,IACjC,CAAC;AAAA,EACF,IACC,EAAE,MAAM,SAAS,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE;AAC3C,SAAO,EAAE,QAAQ,OAAO,EAAE,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE,EAAE;AACpD;AAcO,SAAS,eAAe,QAAgB,OAAgB;AAC9D,SAAO,eAAe,SAAS;AAC/B,QAAM,UAAU,iBAAiB,QAAQ,KAAK;AAC9C,iBAAe,IAAI,QAAQ,OAAO;AAGlC,SAAO,iBAAiB,QAAQ,OAAO,SAAS,UAAU,CAAC,QAAQ,OAAO,OAAO,IAAI,CAAC,CAAC;AACxF;AAKA,SAAS,iBAAiB,QAAgB;AACzC,QAAM,MAAM,qBAAqB,QAAQ,OAAO,OAAO,oBAAoB,CAAC;AAC5E,SAAO,iBAAiB,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC5C;AAEA,MAAM,oBAAoB,UAAU;AAAA,EACnC,OAAgB,KAAK;AAAA,EAEZ,UAAU;AAElB,SAAK,OAAO,UAAU,EAAE,MAAM,WAAW,UAAU,EAAE,CAAC;AAItD,QAAI,eAAe,IAAI,KAAK,MAAM,GAAG,OAAO,SAAS,UAAU;AAC9D,WAAK,OAAO,iBAAiB,CAAC,CAAC;AAAA,IAChC,OAAO;AACN,uBAAiB,KAAK,MAAM;AAAA,IAC7B;AAAA,EACD;AAAA,EAES,gBAAgB;AACxB,qBAAiB,KAAK,MAAM;AAAA,EAC7B;AAAA,EAES,gBAAgB;AAGxB,QAAI,eAAe,IAAI,KAAK,MAAM,GAAG;AACpC,WAAK,OAAO,eAAe,QAAQ;AACnC;AAAA,IACD;AACA,SAAK,OAAO,WAAW,UAAU;AAAA,EAClC;AACD;AAEA,MAAM,wBAAwB,UAAU;AAAA,EACvC,OAAgB,KAAK;AAAA,EAEZ,UAAU;AAIlB,UAAM,EAAE,OAAO,IAAI;AAGnB,WAAO,UAAU,EAAE,MAAM,QAAQ,UAAU,EAAE,CAAC;AAC9C,UAAM,QAAQ,OAAO,OAAO,oBAAoB;AAChD,mBAAe,IAAI,QAAQ;AAAA,MAC1B,QAAQ,EAAE,MAAM,SAAS,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE;AAAA,MAChD,OAAO,EAAE,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE;AAAA,IACjC,CAAC;AAAA,EACF;AAAA,EAES,gBAAgB;AACxB,UAAM,EAAE,OAAO,IAAI;AAGnB,QAAI,qBAAqB,MAAM,EAAE,iBAAiB,OAAO,OAAO,cAAc,GAAG;AAChF,WAAK,OAAO,WAAW,UAAU;AACjC;AAAA,IACD;AAGA,qBAAiB,MAAM;AACvB,UAAM,QAAQ,OAAO,OAAO,oBAAoB;AAChD,mBAAe,OAAO,QAAQ,CAAC,MAAO,IAAI,EAAE,GAAG,GAAG,OAAO,EAAE,GAAG,MAAM,GAAG,GAAG,MAAM,EAAE,EAAE,IAAI,CAAE;AAAA,EAC3F;AAAA;AAAA,EAGS,cAAc;AACtB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,QAAQ,OAAO,OAAO,oBAAoB;AAChD,mBAAe,IAAI,QAAQ,iBAAiB,QAAQ,KAAK,CAAC;AAI1D,SAAK,OAAO,WAAW,MAAM;AAAA,EAC9B;AAAA,EAES,WAAW;AACnB,SAAK,OAAO;AAAA,EACb;AAAA,EAES,cAAc;AACtB,SAAK,OAAO;AAAA,EACb;AAAA;AAAA;AAAA,EAIQ,SAAS;AAChB,SAAK,OAAO,eAAe,QAAQ;AAAA,EACpC;AACD;AAEA,MAAM,wBAAwB,UAAU;AAAA,EACvC,OAAgB,KAAK;AAAA,EAEZ,UAAU;AAIlB,mBAAe,IAAI,KAAK,QAAQ,IAAI;AACpC,SAAK,OAAO,iBAAiB,CAAC,CAAC;AAC/B,SAAK,OAAO,UAAU,EAAE,MAAM,SAAS,UAAU,EAAE,CAAC;AACpD,SAAK,YAAY;AAAA,EAClB;AAAA,EAES,gBAAgB;AACxB,SAAK,YAAY;AAAA,EAClB;AAAA;AAAA,EAGS,cAAc;AACtB,UAAM,EAAE,OAAO,IAAI;AACnB,UAAM,SAAS,OAAO,OAAO,mBAAmB;AAChD,UAAM,UAAU,OAAO,OAAO,oBAAoB;AAClD,UAAM,SAAS,cAAc,QAAQ,OAAO;AAE5C,UAAM,MAAM,EAAE,GAAG,QAAQ,KAAK,OAAO,IAAI,IAAI,GAAG,GAAG,QAAQ,KAAK,OAAO,IAAI,IAAI,EAAE;AACjF,gBAAY,IAAI,QAAQ,IAAI;AAC5B,mBAAe,IAAI,QAAQ;AAAA,MAC1B,QAAQ,EAAE,MAAM,UAAU,GAAG,QAAQ,MAAM,IAAI,GAAG,MAAM,IAAI,EAAE;AAAA,MAC9D,OAAO,eAAe,QAAQ,GAAG;AAAA,IAClC,CAAC;AAED,SAAK,OAAO,WAAW,MAAM;AAAA,EAC9B;AAAA,EAES,WAAW;AACnB,SAAK,OAAO;AAAA,EACb;AAAA,EAES,cAAc;AACtB,SAAK,OAAO;AAAA,EACb;AAAA,EAEQ,cAAc;AACrB,UAAM,EAAE,OAAO,IAAI;AACnB,gBAAY;AAAA,MACX;AAAA,MACA,cAAc,OAAO,OAAO,mBAAmB,GAAG,OAAO,OAAO,oBAAoB,CAAC;AAAA,IACtF;AAAA,EACD;AAAA;AAAA,EAGQ,SAAS;AAChB,SAAK,OAAO,eAAe,QAAQ;AAAA,EACpC;AACD;AAGO,MAAM,eAAe,CAAC,WAAW;AAExC,MAAM,WAAW,eAAe;AAAA,EAC/B,aAAa,EAAE,IAAI,gBAAgB,gBAAgB,UAAU;AAC9D,CAAC;AAOM,MAAM,uBAAsC;AAAA,EAClD,MAAM,QAAQ,OAAO;AACpB,UAAM,UAAU;AAAA,MACf,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,OAAO,SAAS,YAAY;AAAA,MAC5B,KAAK;AAAA,MACL,UAAU,CAAC,WAAW;AAErB,YAAI,WAAW,eAAgB,gBAAe,QAAQ,OAAO,OAAO,mBAAmB,CAAC;AAAA,YACnF,QAAO,eAAe,SAAS;AAAA,MACrC;AAAA,IACD;AACA,WAAO;AAAA,EACR;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import {
|
|
3
|
+
defineMessages,
|
|
3
4
|
TldrawUiButton,
|
|
4
5
|
TldrawUiDropdownMenuContent,
|
|
5
6
|
TldrawUiDropdownMenuRoot,
|
|
@@ -13,6 +14,13 @@ import {
|
|
|
13
14
|
} from "tldraw";
|
|
14
15
|
import { MoreMenuIcon } from "../ui/icons.mjs";
|
|
15
16
|
import { sidebarFilters } from "./state.mjs";
|
|
17
|
+
const messages = defineMessages({
|
|
18
|
+
commentsFilter: { id: "comments.filter", defaultMessage: "Filter comments" },
|
|
19
|
+
commentsOnlyMyComments: { id: "comments.only-my-comments", defaultMessage: "Only my comments" },
|
|
20
|
+
commentsOnlyUnread: { id: "comments.only-unread", defaultMessage: "Only unread" },
|
|
21
|
+
commentsShowAllPages: { id: "comments.show-all-pages", defaultMessage: "Show all pages" },
|
|
22
|
+
commentsShowResolved: { id: "comments.show-resolved", defaultMessage: "Show resolved comments" }
|
|
23
|
+
});
|
|
16
24
|
function CommentsFilterMenu({
|
|
17
25
|
canFilterByAuthor,
|
|
18
26
|
canFilterByUnread
|
|
@@ -28,8 +36,8 @@ function CommentsFilterMenu({
|
|
|
28
36
|
TldrawUiButton,
|
|
29
37
|
{
|
|
30
38
|
type: "icon",
|
|
31
|
-
tooltip: msg(
|
|
32
|
-
title: msg(
|
|
39
|
+
tooltip: msg(messages.commentsFilter),
|
|
40
|
+
title: msg(messages.commentsFilter),
|
|
33
41
|
className: "tlui-cmt-header-btn",
|
|
34
42
|
children: /* @__PURE__ */ jsx(MoreMenuIcon, {})
|
|
35
43
|
}
|
|
@@ -39,7 +47,7 @@ function CommentsFilterMenu({
|
|
|
39
47
|
TldrawUiMenuCheckboxItem,
|
|
40
48
|
{
|
|
41
49
|
id: "show-all-pages",
|
|
42
|
-
label:
|
|
50
|
+
label: messages.commentsShowAllPages.id,
|
|
43
51
|
checked: !filters.onlyCurrentPage,
|
|
44
52
|
onSelect: () => toggle("onlyCurrentPage")
|
|
45
53
|
}
|
|
@@ -48,7 +56,7 @@ function CommentsFilterMenu({
|
|
|
48
56
|
TldrawUiMenuCheckboxItem,
|
|
49
57
|
{
|
|
50
58
|
id: "only-my-comments",
|
|
51
|
-
label:
|
|
59
|
+
label: messages.commentsOnlyMyComments.id,
|
|
52
60
|
checked: filters.onlyMine,
|
|
53
61
|
onSelect: () => toggle("onlyMine")
|
|
54
62
|
}
|
|
@@ -57,7 +65,7 @@ function CommentsFilterMenu({
|
|
|
57
65
|
TldrawUiMenuCheckboxItem,
|
|
58
66
|
{
|
|
59
67
|
id: "only-unread",
|
|
60
|
-
label:
|
|
68
|
+
label: messages.commentsOnlyUnread.id,
|
|
61
69
|
checked: filters.onlyUnread,
|
|
62
70
|
onSelect: () => toggle("onlyUnread")
|
|
63
71
|
}
|
|
@@ -66,7 +74,7 @@ function CommentsFilterMenu({
|
|
|
66
74
|
TldrawUiMenuCheckboxItem,
|
|
67
75
|
{
|
|
68
76
|
id: "show-resolved",
|
|
69
|
-
label:
|
|
77
|
+
label: messages.commentsShowResolved.id,
|
|
70
78
|
checked: filters.showResolved,
|
|
71
79
|
onSelect: () => toggle("showResolved")
|
|
72
80
|
}
|