@tldraw/commenting 5.4.0-canary.b6aebcb84bf1 → 5.4.0-canary.b6b68fdff9e0
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/LICENSE.md +1 -0
- package/dist-cjs/canvas/license.js +1 -1
- package/dist-cjs/canvas/license.js.map +2 -2
- package/dist-cjs/index.d.ts +11 -4
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/ui/emoji-picker.js +1 -1
- package/dist-cjs/ui/emoji-picker.js.map +2 -2
- package/dist-cjs/ui/reaction-picker.js +9 -3
- package/dist-cjs/ui/reaction-picker.js.map +2 -2
- package/dist-esm/canvas/license.mjs +2 -2
- package/dist-esm/canvas/license.mjs.map +2 -2
- package/dist-esm/index.d.mts +11 -4
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/ui/emoji-picker.mjs +1 -1
- package/dist-esm/ui/emoji-picker.mjs.map +2 -2
- package/dist-esm/ui/reaction-picker.mjs +9 -3
- package/dist-esm/ui/reaction-picker.mjs.map +2 -2
- package/package.json +13 -5
- package/src/canvas/license.ts +5 -3
- package/src/ui/emoji-picker.tsx +7 -3
- package/src/ui/reaction-picker.test.ts +36 -0
- package/src/ui/reaction-picker.tsx +15 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This code is licensed under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md)
|
|
@@ -23,6 +23,6 @@ __export(license_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(license_exports);
|
|
24
24
|
var import_tldraw = require("tldraw");
|
|
25
25
|
function useCommentingEnabled() {
|
|
26
|
-
return (0, import_tldraw.useLicenseFeatureFlag)((0, import_tldraw.
|
|
26
|
+
return (0, import_tldraw.useLicenseFeatureFlag)((0, import_tldraw.useMaybeLicenseManager)(), "commenting");
|
|
27
27
|
}
|
|
28
28
|
//# sourceMappingURL=license.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/license.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { useLicenseFeatureFlag, useMaybeLicenseManager } from 'tldraw'\n\n/**\n * Whether commenting is licensed for this editor. Enabled in development; in production it requires\n * a tldraw license that includes the commenting feature (or the collaboration umbrella that grants\n * it). Reactive: re-reads when license validation resolves, and returns `false` while validation is\n * pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:\n * UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at\n * all this is `false`.\n *\n * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment\n * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.\n * @public\n */\nexport function useCommentingEnabled(): boolean {\n\treturn useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA8D;AAcvD,SAAS,uBAAgC;AAC/C,aAAO,yCAAsB,sCAAuB,GAAG,YAAY;AACpE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-cjs/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Editor } from 'tldraw';
|
|
|
7
7
|
import { EditorAtom } from 'tldraw';
|
|
8
8
|
import { filterMentionMembers } from '@tldraw/mentions';
|
|
9
9
|
import { JSX } from 'react/jsx-runtime';
|
|
10
|
+
import { KeyboardEvent as KeyboardEvent_2 } from 'react';
|
|
10
11
|
import { Mention } from '@tldraw/mentions';
|
|
11
12
|
import { MentionList } from '@tldraw/mentions';
|
|
12
13
|
import { MentionListProps } from '@tldraw/mentions';
|
|
@@ -811,8 +812,11 @@ export declare interface EmojiPickerProps {
|
|
|
811
812
|
emoji?: string[];
|
|
812
813
|
/** Emoji the current user has already reacted with; shown as pressed. */
|
|
813
814
|
selected?: string[];
|
|
814
|
-
/**
|
|
815
|
-
|
|
815
|
+
/**
|
|
816
|
+
* Called when an emoji is chosen. Custom palettes should forward their click or keydown event
|
|
817
|
+
* so `ReactionPicker` can keep the palette open on shift-picks.
|
|
818
|
+
*/
|
|
819
|
+
onSelect?(emoji: string, event?: KeyboardEvent_2 | MouseEvent_2): void;
|
|
816
820
|
/** How to draw each emoji token. Defaults to the token string (OS emoji font). */
|
|
817
821
|
renderReaction?: RenderReaction;
|
|
818
822
|
}
|
|
@@ -991,7 +995,8 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
|
|
|
991
995
|
* reactions are added and the row reflows.
|
|
992
996
|
*
|
|
993
997
|
* Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
|
|
994
|
-
* done its job — and hands focus back to the trigger.
|
|
998
|
+
* done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
|
|
999
|
+
* choosing several reactions in a row.
|
|
995
1000
|
* @public @react
|
|
996
1001
|
*/
|
|
997
1002
|
export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
|
|
@@ -1352,7 +1357,9 @@ export declare function useCanModifyComment(currentUserId: null | string | undef
|
|
|
1352
1357
|
* Whether commenting is licensed for this editor. Enabled in development; in production it requires
|
|
1353
1358
|
* a tldraw license that includes the commenting feature (or the collaboration umbrella that grants
|
|
1354
1359
|
* it). Reactive: re-reads when license validation resolves, and returns `false` while validation is
|
|
1355
|
-
* pending, so gated UI stays hidden until the license is confirmed.
|
|
1360
|
+
* pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:
|
|
1361
|
+
* UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at
|
|
1362
|
+
* all this is `false`.
|
|
1356
1363
|
*
|
|
1357
1364
|
* The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment
|
|
1358
1365
|
* tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.
|
package/dist-cjs/index.js
CHANGED
|
@@ -144,7 +144,7 @@ var import_state = require("./canvas/state");
|
|
|
144
144
|
var import_thread_state = require("./canvas/thread-state");
|
|
145
145
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
146
146
|
"@tldraw/commenting",
|
|
147
|
-
"5.4.0-canary.
|
|
147
|
+
"5.4.0-canary.b6b68fdff9e0",
|
|
148
148
|
"cjs"
|
|
149
149
|
);
|
|
150
150
|
//# sourceMappingURL=index.js.map
|
|
@@ -44,7 +44,7 @@ function EmojiPicker({
|
|
|
44
44
|
className: active ? "tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active" : "tlui-cmt-emoji-picker__item",
|
|
45
45
|
"aria-label": value,
|
|
46
46
|
"aria-pressed": active,
|
|
47
|
-
onClick: () => onSelect?.(value),
|
|
47
|
+
onClick: (e) => onSelect?.(value, e),
|
|
48
48
|
children: renderReaction(value)
|
|
49
49
|
},
|
|
50
50
|
value
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/emoji-picker.tsx"],
|
|
4
|
-
"sourcesContent": ["import { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\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. */\n\tselected?: string[]\n\t
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { type KeyboardEvent, type MouseEvent } from 'react'\nimport { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\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. */\n\tselected?: string[]\n\t/**\n\t * Called when an emoji is chosen. Custom palettes should forward their click or keydown event\n\t * so `ReactionPicker` can keep the palette open on shift-picks.\n\t */\n\tonSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={(e) => onSelect?.(value, e)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA6DK;AA5DL,sBAAsD;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAsBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,4CAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,CAAC,MAAM,WAAW,OAAO,CAAC;AAAA,QAElC,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -18,7 +18,8 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var reaction_picker_exports = {};
|
|
20
20
|
__export(reaction_picker_exports, {
|
|
21
|
-
ReactionPicker: () => ReactionPicker
|
|
21
|
+
ReactionPicker: () => ReactionPicker,
|
|
22
|
+
isMultiSelectPick: () => isMultiSelectPick
|
|
22
23
|
});
|
|
23
24
|
module.exports = __toCommonJS(reaction_picker_exports);
|
|
24
25
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -26,6 +27,9 @@ var import_react = require("react");
|
|
|
26
27
|
var import_tldraw = require("tldraw");
|
|
27
28
|
var import_emoji_picker = require("./emoji-picker");
|
|
28
29
|
var import_icons = require("./icons");
|
|
30
|
+
function isMultiSelectPick(event) {
|
|
31
|
+
return !!event?.shiftKey;
|
|
32
|
+
}
|
|
29
33
|
function ReactionPicker({
|
|
30
34
|
emoji,
|
|
31
35
|
selected,
|
|
@@ -40,9 +44,11 @@ function ReactionPicker({
|
|
|
40
44
|
const generatedId = (0, import_react.useId)();
|
|
41
45
|
const id = menuId ?? `comment-reactions-${generatedId}`;
|
|
42
46
|
const handleSelect = (0, import_react.useCallback)(
|
|
43
|
-
(value) => {
|
|
47
|
+
(value, event) => {
|
|
44
48
|
onSelect?.(value);
|
|
45
|
-
|
|
49
|
+
if (!isMultiSelectPick(event)) {
|
|
50
|
+
import_tldraw.tlmenus.deleteOpenMenu(id, editor?.contextId);
|
|
51
|
+
}
|
|
46
52
|
},
|
|
47
53
|
[onSelect, id, editor]
|
|
48
54
|
);
|
|
@@ -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 } 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 * 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.\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) => {\n\t\t\tonSelect?.(value)\n\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\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('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\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;
|
|
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('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\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;AA0FE;AA1FF,mBAA4F;AAC5F,oBAQO;AACP,0BAAmD;AACnD,mBAA2B;AAmCpB,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,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLicenseFeatureFlag, useMaybeLicenseManager } from "tldraw";
|
|
2
2
|
function useCommentingEnabled() {
|
|
3
|
-
return useLicenseFeatureFlag(
|
|
3
|
+
return useLicenseFeatureFlag(useMaybeLicenseManager(), "commenting");
|
|
4
4
|
}
|
|
5
5
|
export {
|
|
6
6
|
useCommentingEnabled
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/license.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
4
|
+
"sourcesContent": ["import { useLicenseFeatureFlag, useMaybeLicenseManager } from 'tldraw'\n\n/**\n * Whether commenting is licensed for this editor. Enabled in development; in production it requires\n * a tldraw license that includes the commenting feature (or the collaboration umbrella that grants\n * it). Reactive: re-reads when license validation resolves, and returns `false` while validation is\n * pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:\n * UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at\n * all this is `false`.\n *\n * The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment\n * tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.\n * @public\n */\nexport function useCommentingEnabled(): boolean {\n\treturn useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB,8BAA8B;AAcvD,SAAS,uBAAgC;AAC/C,SAAO,sBAAsB,uBAAuB,GAAG,YAAY;AACpE;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -7,6 +7,7 @@ import { Editor } from 'tldraw';
|
|
|
7
7
|
import { EditorAtom } from 'tldraw';
|
|
8
8
|
import { filterMentionMembers } from '@tldraw/mentions';
|
|
9
9
|
import { JSX } from 'react/jsx-runtime';
|
|
10
|
+
import { KeyboardEvent as KeyboardEvent_2 } from 'react';
|
|
10
11
|
import { Mention } from '@tldraw/mentions';
|
|
11
12
|
import { MentionList } from '@tldraw/mentions';
|
|
12
13
|
import { MentionListProps } from '@tldraw/mentions';
|
|
@@ -811,8 +812,11 @@ export declare interface EmojiPickerProps {
|
|
|
811
812
|
emoji?: string[];
|
|
812
813
|
/** Emoji the current user has already reacted with; shown as pressed. */
|
|
813
814
|
selected?: string[];
|
|
814
|
-
/**
|
|
815
|
-
|
|
815
|
+
/**
|
|
816
|
+
* Called when an emoji is chosen. Custom palettes should forward their click or keydown event
|
|
817
|
+
* so `ReactionPicker` can keep the palette open on shift-picks.
|
|
818
|
+
*/
|
|
819
|
+
onSelect?(emoji: string, event?: KeyboardEvent_2 | MouseEvent_2): void;
|
|
816
820
|
/** How to draw each emoji token. Defaults to the token string (OS emoji font). */
|
|
817
821
|
renderReaction?: RenderReaction;
|
|
818
822
|
}
|
|
@@ -991,7 +995,8 @@ export declare function Reaction({ emoji, count, active, reactors, enableHoverLi
|
|
|
991
995
|
* reactions are added and the row reflows.
|
|
992
996
|
*
|
|
993
997
|
* Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
|
|
994
|
-
* done its job — and hands focus back to the trigger.
|
|
998
|
+
* done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
|
|
999
|
+
* choosing several reactions in a row.
|
|
995
1000
|
* @public @react
|
|
996
1001
|
*/
|
|
997
1002
|
export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
|
|
@@ -1352,7 +1357,9 @@ export declare function useCanModifyComment(currentUserId: null | string | undef
|
|
|
1352
1357
|
* Whether commenting is licensed for this editor. Enabled in development; in production it requires
|
|
1353
1358
|
* a tldraw license that includes the commenting feature (or the collaboration umbrella that grants
|
|
1354
1359
|
* it). Reactive: re-reads when license validation resolves, and returns `false` while validation is
|
|
1355
|
-
* pending, so gated UI stays hidden until the license is confirmed.
|
|
1360
|
+
* pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:
|
|
1361
|
+
* UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at
|
|
1362
|
+
* all this is `false`.
|
|
1356
1363
|
*
|
|
1357
1364
|
* The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment
|
|
1358
1365
|
* tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.
|
package/dist-esm/index.mjs
CHANGED
|
@@ -103,7 +103,7 @@ import {
|
|
|
103
103
|
import { anchorPagePoint, focusThread, shapeAnchorAt } from "./canvas/thread-state.mjs";
|
|
104
104
|
registerTldrawLibraryVersion(
|
|
105
105
|
"@tldraw/commenting",
|
|
106
|
-
"5.4.0-canary.
|
|
106
|
+
"5.4.0-canary.b6b68fdff9e0",
|
|
107
107
|
"esm"
|
|
108
108
|
);
|
|
109
109
|
export {
|
|
@@ -19,7 +19,7 @@ function EmojiPicker({
|
|
|
19
19
|
className: active ? "tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active" : "tlui-cmt-emoji-picker__item",
|
|
20
20
|
"aria-label": value,
|
|
21
21
|
"aria-pressed": active,
|
|
22
|
-
onClick: () => onSelect?.(value),
|
|
22
|
+
onClick: (e) => onSelect?.(value, e),
|
|
23
23
|
children: renderReaction(value)
|
|
24
24
|
},
|
|
25
25
|
value
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/emoji-picker.tsx"],
|
|
4
|
-
"sourcesContent": ["import { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\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. */\n\tselected?: string[]\n\t
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { type KeyboardEvent, type MouseEvent } from 'react'\nimport { RenderReaction, defaultRenderReaction } from './reaction'\n\n/**\n * The reaction emoji palette: the small, opinionated set the picker offers *and* the set a\n * reaction is allowed to use. It's deliberately short \u2014 a quick affordance under a comment, not a\n * full emoji keyboard \u2014 and it's the single source of truth for \"which emoji may a reaction\n * carry\". A reaction's `emoji` field is a free-form `string`, so this palette is what keeps it to\n * real, offered emoji; check membership with {@link isAllowedReactionEmoji}. Pass a custom `emoji`\n * array to `ReactionPicker` to offer a different set \u2014 validate against that same set if you do.\n *\n * @public\n */\nexport const DEFAULT_REACTION_EMOJI = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2764\uFE0F', '\uD83C\uDF89', '\uD83D\uDE04', '\uD83D\uDE2E', '\uD83D\uDE22', '\uD83D\uDE4F', '\uD83D\uDC40', '\uD83D\uDD25']\n\n/**\n * Whether `emoji` belongs to a reaction palette (defaults to {@link DEFAULT_REACTION_EMOJI}). Use\n * it to reject arbitrary strings before writing a reaction, so a scripted client can't spam a\n * comment with junk `emoji` values that the picker would never offer. Pass a custom `palette` to\n * match a customized picker.\n *\n * @public\n */\nexport function isAllowedReactionEmoji(\n\temoji: string,\n\tpalette: readonly string[] = DEFAULT_REACTION_EMOJI\n): boolean {\n\treturn palette.includes(emoji)\n}\n\n/** @public */\nexport interface EmojiPickerProps {\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. */\n\tselected?: string[]\n\t/**\n\t * Called when an emoji is chosen. Custom palettes should forward their click or keydown event\n\t * so `ReactionPicker` can keep the palette open on shift-picks.\n\t */\n\tonSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void\n\t/** How to draw each emoji token. Defaults to the token string (OS emoji font). */\n\trenderReaction?: RenderReaction\n}\n\n/**\n * A grid of emoji to react with. Presentational and unpositioned \u2014 the host owns placement and\n * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.\n * @public @react\n */\nexport function EmojiPicker({\n\temoji = DEFAULT_REACTION_EMOJI,\n\tselected,\n\tonSelect,\n\trenderReaction = defaultRenderReaction,\n}: EmojiPickerProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-emoji-picker\" role=\"group\">\n\t\t\t{emoji.map((value) => {\n\t\t\t\tconst active = selected?.includes(value) ?? false\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tactive\n\t\t\t\t\t\t\t\t? 'tlui-cmt-emoji-picker__item tlui-cmt-emoji-picker__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-emoji-picker__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\taria-label={value}\n\t\t\t\t\t\taria-pressed={active}\n\t\t\t\t\t\tonClick={(e) => onSelect?.(value, e)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderReaction(value)}\n\t\t\t\t\t</button>\n\t\t\t\t)\n\t\t\t})}\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AA6DK;AA5DL,SAAyB,6BAA6B;AAY/C,MAAM,yBAAyB,CAAC,aAAM,aAAM,gBAAM,aAAM,aAAM,aAAM,aAAM,aAAM,aAAM,WAAI;AAU1F,SAAS,uBACf,OACA,UAA6B,wBACnB;AACV,SAAO,QAAQ,SAAS,KAAK;AAC9B;AAsBO,SAAS,YAAY;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,iBAAiB;AAClB,GAAqB;AACpB,SACC,oBAAC,SAAI,WAAU,yBAAwB,MAAK,SAC1C,gBAAM,IAAI,CAAC,UAAU;AACrB,UAAM,SAAS,UAAU,SAAS,KAAK,KAAK;AAC5C,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,WACC,SACG,oEACA;AAAA,QAEJ,cAAY;AAAA,QACZ,gBAAc;AAAA,QACd,SAAS,CAAC,MAAM,WAAW,OAAO,CAAC;AAAA,QAElC,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
} from "tldraw";
|
|
12
12
|
import { EmojiPicker } from "./emoji-picker.mjs";
|
|
13
13
|
import { SmileyIcon } from "./icons.mjs";
|
|
14
|
+
function isMultiSelectPick(event) {
|
|
15
|
+
return !!event?.shiftKey;
|
|
16
|
+
}
|
|
14
17
|
function ReactionPicker({
|
|
15
18
|
emoji,
|
|
16
19
|
selected,
|
|
@@ -25,9 +28,11 @@ function ReactionPicker({
|
|
|
25
28
|
const generatedId = useId();
|
|
26
29
|
const id = menuId ?? `comment-reactions-${generatedId}`;
|
|
27
30
|
const handleSelect = useCallback(
|
|
28
|
-
(value) => {
|
|
31
|
+
(value, event) => {
|
|
29
32
|
onSelect?.(value);
|
|
30
|
-
|
|
33
|
+
if (!isMultiSelectPick(event)) {
|
|
34
|
+
tlmenus.deleteOpenMenu(id, editor?.contextId);
|
|
35
|
+
}
|
|
31
36
|
},
|
|
32
37
|
[onSelect, id, editor]
|
|
33
38
|
);
|
|
@@ -54,6 +59,7 @@ function ReactionPicker({
|
|
|
54
59
|
] });
|
|
55
60
|
}
|
|
56
61
|
export {
|
|
57
|
-
ReactionPicker
|
|
62
|
+
ReactionPicker,
|
|
63
|
+
isMultiSelectPick
|
|
58
64
|
};
|
|
59
65
|
//# sourceMappingURL=reaction-picker.mjs.map
|
|
@@ -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 } 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 * 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.\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) => {\n\t\t\tonSelect?.(value)\n\t\t\ttlmenus.deleteOpenMenu(id, editor?.contextId)\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('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\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": "
|
|
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('comments.add-reaction')}\n\t\t\t\t\ttitle={msg('comments.add-reaction')}\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": "AA0FE,SAQG,KARH;AA1FF,SAAS,aAAa,aAAsE;AAC5F;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;AAmCpB,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,MAAM,eAAe;AAC3B,QAAM,SAAS,eAAe;AAE9B,QAAM,cAAc,MAAM;AAC1B,QAAM,KAAK,UAAU,qBAAqB,WAAW;AAKrD,QAAM,eAAe;AAAA,IACpB,CAAC,OAAe,UAAuC;AACtD,iBAAW,KAAK;AAChB,UAAI,CAAC,kBAAkB,KAAK,GAAG;AAC9B,gBAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACD;AAAA,IACA,CAAC,UAAU,IAAI,MAAM;AAAA,EACtB;AAEA,SACC,qBAAC,4BAAyB,IACzB;AAAA,wBAAC,+BACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,uBAAuB;AAAA,QACpC,OAAO,IAAI,uBAAuB;AAAA,QAClC;AAAA,QAEA,8BAAC,cAAW;AAAA;AAAA,IACb,GACD;AAAA,IAEA,oBAAC,+BAA4B,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/package.json
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/commenting",
|
|
3
3
|
"description": "tldraw commenting UI components.",
|
|
4
|
-
"version": "5.4.0-canary.
|
|
4
|
+
"version": "5.4.0-canary.b6b68fdff9e0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
8
8
|
},
|
|
9
9
|
"homepage": "https://tldraw.dev",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
11
|
+
"tldraw_product": {
|
|
12
|
+
"stableId": "tldraw:commenting",
|
|
13
|
+
"name": "Commenting",
|
|
14
|
+
"type": "feature",
|
|
15
|
+
"parent": "tldraw:collaboration",
|
|
16
|
+
"premium": true,
|
|
17
|
+
"licenseFlag": "FEAT_COMMENTING"
|
|
18
|
+
},
|
|
11
19
|
"repository": {
|
|
12
20
|
"type": "git",
|
|
13
21
|
"url": "https://github.com/tldraw/tldraw"
|
|
@@ -38,7 +46,7 @@
|
|
|
38
46
|
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
39
47
|
},
|
|
40
48
|
"devDependencies": {
|
|
41
|
-
"@tldraw/driver": "5.4.0-canary.
|
|
49
|
+
"@tldraw/driver": "5.4.0-canary.b6b68fdff9e0",
|
|
42
50
|
"@types/react": "^19.2.7",
|
|
43
51
|
"@types/react-dom": "^19.2.3",
|
|
44
52
|
"react": "^19.2.1",
|
|
@@ -56,9 +64,9 @@
|
|
|
56
64
|
"@tiptap/core": "^3.12.1",
|
|
57
65
|
"@tiptap/pm": "^3.12.1",
|
|
58
66
|
"@tiptap/react": "^3.12.1",
|
|
59
|
-
"@tldraw/mentions": "5.4.0-canary.
|
|
60
|
-
"@tldraw/utils": "5.4.0-canary.
|
|
61
|
-
"tldraw": "5.4.0-canary.
|
|
67
|
+
"@tldraw/mentions": "5.4.0-canary.b6b68fdff9e0",
|
|
68
|
+
"@tldraw/utils": "5.4.0-canary.b6b68fdff9e0",
|
|
69
|
+
"tldraw": "5.4.0-canary.b6b68fdff9e0"
|
|
62
70
|
},
|
|
63
71
|
"module": "dist-esm/index.mjs",
|
|
64
72
|
"source": "src/index.ts",
|
package/src/canvas/license.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useLicenseFeatureFlag, useMaybeLicenseManager } from 'tldraw'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Whether commenting is licensed for this editor. Enabled in development; in production it requires
|
|
5
5
|
* a tldraw license that includes the commenting feature (or the collaboration umbrella that grants
|
|
6
6
|
* it). Reactive: re-reads when license validation resolves, and returns `false` while validation is
|
|
7
|
-
* pending, so gated UI stays hidden until the license is confirmed.
|
|
7
|
+
* pending, so gated UI stays hidden until the license is confirmed. Works outside `<Tldraw />` too:
|
|
8
|
+
* UI mounted via `EditorProvider` resolves the license through the editor, and with no editor at
|
|
9
|
+
* all this is `false`.
|
|
8
10
|
*
|
|
9
11
|
* The built-in commenting components (`CanvasComments`, `CanvasCommentsSidebar`, and the comment
|
|
10
12
|
* tool's Quick Action) gate on this. Use it to gate any custom commenting UI the same way.
|
|
11
13
|
* @public
|
|
12
14
|
*/
|
|
13
15
|
export function useCommentingEnabled(): boolean {
|
|
14
|
-
return useLicenseFeatureFlag(
|
|
16
|
+
return useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')
|
|
15
17
|
}
|
package/src/ui/emoji-picker.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyboardEvent, type MouseEvent } from 'react'
|
|
1
2
|
import { RenderReaction, defaultRenderReaction } from './reaction'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -33,8 +34,11 @@ export interface EmojiPickerProps {
|
|
|
33
34
|
emoji?: string[]
|
|
34
35
|
/** Emoji the current user has already reacted with; shown as pressed. */
|
|
35
36
|
selected?: string[]
|
|
36
|
-
/**
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Called when an emoji is chosen. Custom palettes should forward their click or keydown event
|
|
39
|
+
* so `ReactionPicker` can keep the palette open on shift-picks.
|
|
40
|
+
*/
|
|
41
|
+
onSelect?(emoji: string, event?: MouseEvent | KeyboardEvent): void
|
|
38
42
|
/** How to draw each emoji token. Defaults to the token string (OS emoji font). */
|
|
39
43
|
renderReaction?: RenderReaction
|
|
40
44
|
}
|
|
@@ -65,7 +69,7 @@ export function EmojiPicker({
|
|
|
65
69
|
}
|
|
66
70
|
aria-label={value}
|
|
67
71
|
aria-pressed={active}
|
|
68
|
-
onClick={() => onSelect?.(value)}
|
|
72
|
+
onClick={(e) => onSelect?.(value, e)}
|
|
69
73
|
>
|
|
70
74
|
{renderReaction(value)}
|
|
71
75
|
</button>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { MouseEvent } from 'react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { isMultiSelectPick } from './reaction-picker'
|
|
4
|
+
|
|
5
|
+
function click(overrides: Partial<MouseEvent> = {}): MouseEvent {
|
|
6
|
+
return {
|
|
7
|
+
metaKey: false,
|
|
8
|
+
ctrlKey: false,
|
|
9
|
+
shiftKey: false,
|
|
10
|
+
altKey: false,
|
|
11
|
+
button: 0,
|
|
12
|
+
...overrides,
|
|
13
|
+
} as MouseEvent
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('isMultiSelectPick', () => {
|
|
17
|
+
it('keeps the palette open on a shift pick', () => {
|
|
18
|
+
expect(isMultiSelectPick(click({ shiftKey: true }))).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('closes on a plain pick', () => {
|
|
22
|
+
expect(isMultiSelectPick(click())).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// custom palettes may call onSelect without an event; that must keep today's close-on-pick
|
|
26
|
+
it('closes when no event is given', () => {
|
|
27
|
+
expect(isMultiSelectPick(undefined)).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it.each(['metaKey', 'ctrlKey', 'altKey'] as const)(
|
|
31
|
+
'does not treat a %s pick as multi-select',
|
|
32
|
+
(modifier) => {
|
|
33
|
+
expect(isMultiSelectPick(click({ [modifier]: true }))).toBe(false)
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useId, type ComponentType } from 'react'
|
|
1
|
+
import { useCallback, useId, type ComponentType, type KeyboardEvent, type MouseEvent } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
TldrawUiButton,
|
|
4
4
|
TldrawUiDropdownMenuContent,
|
|
@@ -40,6 +40,14 @@ export interface ReactionPickerProps {
|
|
|
40
40
|
className?: string
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether a palette pick asked to keep the palette open (shift held) for multi-select.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function isMultiSelectPick(event?: MouseEvent | KeyboardEvent): boolean {
|
|
48
|
+
return !!event?.shiftKey
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
/**
|
|
44
52
|
* The add-reaction affordance: a smiley button that opens an emoji grid.
|
|
45
53
|
*
|
|
@@ -47,7 +55,8 @@ export interface ReactionPickerProps {
|
|
|
47
55
|
* reactions are added and the row reflows.
|
|
48
56
|
*
|
|
49
57
|
* Picking an emoji dismisses the grid — adding and removing alike, since either way the picker has
|
|
50
|
-
* done its job — and hands focus back to the trigger.
|
|
58
|
+
* done its job — and hands focus back to the trigger. Shift-picking keeps the grid open, for
|
|
59
|
+
* choosing several reactions in a row.
|
|
51
60
|
* @public @react
|
|
52
61
|
*/
|
|
53
62
|
export function ReactionPicker({
|
|
@@ -69,9 +78,11 @@ export function ReactionPicker({
|
|
|
69
78
|
// what closes it. The palette's tokens are plain buttons — a swappable component, not radix
|
|
70
79
|
// menu items — so nothing dismisses the menu on its own.
|
|
71
80
|
const handleSelect = useCallback(
|
|
72
|
-
(value: string) => {
|
|
81
|
+
(value: string, event?: MouseEvent | KeyboardEvent) => {
|
|
73
82
|
onSelect?.(value)
|
|
74
|
-
|
|
83
|
+
if (!isMultiSelectPick(event)) {
|
|
84
|
+
tlmenus.deleteOpenMenu(id, editor?.contextId)
|
|
85
|
+
}
|
|
75
86
|
},
|
|
76
87
|
[onSelect, id, editor]
|
|
77
88
|
)
|