@tldraw/commenting 5.4.0-canary.e10ff229bb14 → 5.4.0-canary.ea38889d4bb7

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 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.useLicenseContext)(), "commenting");
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 { useLicenseContext, useLicenseFeatureFlag } 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.\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(useLicenseContext(), 'commenting')\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAyD;AAYlD,SAAS,uBAAgC;AAC/C,aAAO,yCAAsB,iCAAkB,GAAG,YAAY;AAC/D;",
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
  }
@@ -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';
@@ -128,7 +129,7 @@ export declare interface CommentBodyProps {
128
129
  }
129
130
 
130
131
  /** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
131
- export declare function CommentCard({ author, body, date, you, edited, actions, footer }: CommentCardProps): JSX.Element;
132
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
133
 
133
134
  /** @public */
134
135
  export declare interface CommentCardProps {
@@ -154,7 +155,7 @@ export declare interface CommentCardProps {
154
155
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
156
  * @public @react
156
157
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
158
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
159
 
159
160
  /** @public */
160
161
  export declare interface CommentComposerProps {
@@ -398,7 +399,7 @@ export declare interface CommentingOptions {
398
399
  }
399
400
 
400
401
  /** One thread's row in a {@link CommentsList}. @public @react */
401
- export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect }: CommentListItemRenderProps): JSX.Element;
402
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
403
 
403
404
  /** @public */
404
405
  export declare interface CommentListItemProps {
@@ -490,7 +491,7 @@ export declare interface CommentPinProps {
490
491
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
492
  * @public @react
492
493
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
494
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
495
 
495
496
  /** @public */
496
497
  export declare interface CommentReactionPickerProps {
@@ -520,7 +521,7 @@ export declare interface CommentReactionsProps {
520
521
 
521
522
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
523
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
524
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
525
 
525
526
  /** @public */
526
527
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +544,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
544
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
545
  * @public @react
545
546
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
547
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
548
 
548
549
  /** @public */
549
550
  export declare interface CommentsListProps {
@@ -598,7 +599,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
599
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
600
  * @public @react
600
601
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
602
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
603
 
603
604
  /** @public */
604
605
  export declare interface CommentThreadProps {
@@ -661,7 +662,7 @@ export declare class CommentTool extends StateNode {
661
662
  export declare const commentToolOverrides: TLUiOverrides;
662
663
 
663
664
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
665
+ export declare const commentTools: (typeof CommentTool)[];
665
666
 
666
667
  /** @public @react */
667
668
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +717,7 @@ export declare const defaultCommentingOptions: {
716
717
  readonly dragHistory: undefined;
717
718
  readonly enableClustering: true;
718
719
  readonly enableRegions: false;
719
- readonly history: "ignore";
720
+ readonly history: 'ignore';
720
721
  readonly impreciseShapeAnchor: {
721
722
  readonly x: 1;
722
723
  readonly y: 0;
@@ -803,7 +804,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
804
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
805
  * @public @react
805
806
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
807
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
808
 
808
809
  /** @public */
809
810
  export declare interface EmojiPickerProps {
@@ -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
- /** Called when an emoji is chosen. */
815
- onSelect?(emoji: string): void;
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
  }
@@ -982,7 +986,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
986
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
987
  * @public @react
984
988
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
989
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
990
 
987
991
  /**
988
992
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -991,10 +995,11 @@ 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
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
1002
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
1003
 
999
1004
  /** @public */
1000
1005
  export declare interface ReactionPickerProps {
@@ -1062,7 +1067,7 @@ export declare interface ReactionReactor {
1062
1067
  * fixed as reactions are added here.
1063
1068
  * @public @react
1064
1069
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1070
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1071
 
1067
1072
  /** @public */
1068
1073
  export declare interface ReactionsProps {
@@ -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.e10ff229bb14",
147
+ "5.4.0-canary.ea38889d4bb7",
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/** 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}\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={() => onSelect?.(value)}\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;AAyDK;AAzDL,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;AAmBO,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,MAAM,WAAW,KAAK;AAAA,QAE9B,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
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
- import_tldraw.tlmenus.deleteOpenMenu(id, editor?.contextId);
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;AA+EE;AA/EF,mBAAuD;AACvD,oBAQO;AACP,0BAAmD;AACnD,mBAA2B;AAyCpB,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,UAAkB;AAClB,iBAAW,KAAK;AAChB,4BAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,IAC7C;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;",
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 { useLicenseContext, useLicenseFeatureFlag } from "tldraw";
1
+ import { useLicenseFeatureFlag, useMaybeLicenseManager } from "tldraw";
2
2
  function useCommentingEnabled() {
3
- return useLicenseFeatureFlag(useLicenseContext(), "commenting");
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 { useLicenseContext, useLicenseFeatureFlag } 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.\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(useLicenseContext(), 'commenting')\n}\n"],
5
- "mappings": "AAAA,SAAS,mBAAmB,6BAA6B;AAYlD,SAAS,uBAAgC;AAC/C,SAAO,sBAAsB,kBAAkB,GAAG,YAAY;AAC/D;",
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
  }
@@ -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';
@@ -128,7 +129,7 @@ export declare interface CommentBodyProps {
128
129
  }
129
130
 
130
131
  /** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
131
- export declare function CommentCard({ author, body, date, you, edited, actions, footer }: CommentCardProps): JSX.Element;
132
+ export declare function CommentCard({ author, body, date, you, edited, actions, footer, }: CommentCardProps): JSX.Element;
132
133
 
133
134
  /** @public */
134
135
  export declare interface CommentCardProps {
@@ -154,7 +155,7 @@ export declare interface CommentCardProps {
154
155
  * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.
155
156
  * @public @react
156
157
  */
157
- export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion }: CommentComposerProps): JSX.Element;
158
+ export declare function CommentComposer({ author, placeholder, value, onChange, onSubmit, sendLabel, onArrowUpWhenEmpty, disabled, autoFocus, leading, getMentionSuggestions, renderMentionSuggestion, }: CommentComposerProps): JSX.Element;
158
159
 
159
160
  /** @public */
160
161
  export declare interface CommentComposerProps {
@@ -398,7 +399,7 @@ export declare interface CommentingOptions {
398
399
  }
399
400
 
400
401
  /** One thread's row in a {@link CommentsList}. @public @react */
401
- export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect }: CommentListItemRenderProps): JSX.Element;
402
+ export declare function CommentListItem({ id, author, preview, date, resolved, page, count, selected, reactions, href, resolvedLabel, onSelect, }: CommentListItemRenderProps): JSX.Element;
402
403
 
403
404
  /** @public */
404
405
  export declare interface CommentListItemProps {
@@ -490,7 +491,7 @@ export declare interface CommentPinProps {
490
491
  * than in the reaction row, so opening it doesn't chase the row as reactions are added.
491
492
  * @public @react
492
493
  */
493
- export declare function CommentReactionPicker({ comment, currentUserId, emoji }: CommentReactionPickerProps): JSX.Element | null;
494
+ export declare function CommentReactionPicker({ comment, currentUserId, emoji, }: CommentReactionPickerProps): JSX.Element | null;
494
495
 
495
496
  /** @public */
496
497
  export declare interface CommentReactionPickerProps {
@@ -520,7 +521,7 @@ export declare interface CommentReactionsProps {
520
521
 
521
522
  /** The funnel dropdown in the sidebar header: toggles for which threads the list shows.
522
523
  * @public @react */
523
- export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread }: CommentsFilterMenuProps): JSX.Element;
524
+ export declare function CommentsFilterMenu({ canFilterByAuthor, canFilterByUnread, }: CommentsFilterMenuProps): JSX.Element;
524
525
 
525
526
  /** @public */
526
527
  export declare interface CommentsFilterMenuProps {
@@ -543,7 +544,7 @@ export declare const commentsHidden: EditorAtom<boolean>;
543
544
  * `CanvasCommentsSidebar` wires it to the store, but a consumer can build their own list from this.
544
545
  * @public @react
545
546
  */
546
- export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem }: CommentsListProps): JSX.Element;
547
+ export declare function CommentsList({ items, onSelect, header, headerAction, empty, resolvedLabel, renderItem, }: CommentsListProps): JSX.Element;
547
548
 
548
549
  /** @public */
549
550
  export declare interface CommentsListProps {
@@ -598,7 +599,7 @@ export declare function CommentsVisibilityToggle(): JSX.Element;
598
599
  * composer's handlers; how each comment renders is overridable via `renderComment`.
599
600
  * @public @react
600
601
  */
601
- export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment }: CommentThreadProps): JSX.Element;
602
+ export declare function CommentThread({ comments, header, headerActions, resolvedBanner, composer, footer, renderComment, }: CommentThreadProps): JSX.Element;
602
603
 
603
604
  /** @public */
604
605
  export declare interface CommentThreadProps {
@@ -661,7 +662,7 @@ export declare class CommentTool extends StateNode {
661
662
  export declare const commentToolOverrides: TLUiOverrides;
662
663
 
663
664
  /** @public */
664
- export declare const commentTools: typeof CommentTool[];
665
+ export declare const commentTools: (typeof CommentTool)[];
665
666
 
666
667
  /** @public @react */
667
668
  export declare function CountBadge({ count, open }: CountBadgeProps): JSX.Element;
@@ -716,7 +717,7 @@ export declare const defaultCommentingOptions: {
716
717
  readonly dragHistory: undefined;
717
718
  readonly enableClustering: true;
718
719
  readonly enableRegions: false;
719
- readonly history: "ignore";
720
+ readonly history: 'ignore';
720
721
  readonly impreciseShapeAnchor: {
721
722
  readonly x: 1;
722
723
  readonly y: 0;
@@ -803,7 +804,7 @@ export declare function editComment(editor: Editor, comment: TLComment, body: TL
803
804
  * dismissal; `ReactionPicker` renders it in a dropdown under the add-reaction button.
804
805
  * @public @react
805
806
  */
806
- export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction }: EmojiPickerProps): JSX.Element;
807
+ export declare function EmojiPicker({ emoji, selected, onSelect, renderReaction, }: EmojiPickerProps): JSX.Element;
807
808
 
808
809
  /** @public */
809
810
  export declare interface EmojiPickerProps {
@@ -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
- /** Called when an emoji is chosen. */
815
- onSelect?(emoji: string): void;
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
  }
@@ -982,7 +986,7 @@ export declare function putCommentRecords(editor: Editor, records: TLCommentReco
982
986
  * tooltip naming who reacted (the current user included), when `enableHoverList` is set.
983
987
  * @public @react
984
988
  */
985
- export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick }: ReactionProps): JSX.Element;
989
+ export declare function Reaction({ emoji, count, active, reactors, enableHoverList, renderReaction, ReactionTooltip, onClick, }: ReactionProps): JSX.Element;
986
990
 
987
991
  /**
988
992
  * The add-reaction affordance: a smiley button that opens an emoji grid.
@@ -991,10 +995,11 @@ 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
- export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className }: ReactionPickerProps): JSX.Element;
1002
+ export declare function ReactionPicker({ emoji, selected, onSelect, renderReaction, palette: Palette, menuId, className, }: ReactionPickerProps): JSX.Element;
998
1003
 
999
1004
  /** @public */
1000
1005
  export declare interface ReactionPickerProps {
@@ -1062,7 +1067,7 @@ export declare interface ReactionReactor {
1062
1067
  * fixed as reactions are added here.
1063
1068
  * @public @react
1064
1069
  */
1065
- export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip }: ReactionsProps): JSX.Element | null;
1070
+ export declare function Reactions({ reactions, onToggle, canReact, enableHoverList, renderReaction, ReactionTooltip, }: ReactionsProps): JSX.Element | null;
1066
1071
 
1067
1072
  /** @public */
1068
1073
  export declare interface ReactionsProps {
@@ -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.
@@ -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.e10ff229bb14",
106
+ "5.4.0-canary.ea38889d4bb7",
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/** 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}\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={() => onSelect?.(value)}\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": "AAyDK;AAzDL,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;AAmBO,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,MAAM,WAAW,KAAK;AAAA,QAE9B,yBAAe,KAAK;AAAA;AAAA,MAXhB;AAAA,IAYN;AAAA,EAEF,CAAC,GACF;AAEF;",
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
- tlmenus.deleteOpenMenu(id, editor?.contextId);
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": "AA+EE,SAQG,KARH;AA/EF,SAAS,aAAa,aAAiC;AACvD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AACP,SAAS,mBAA0C;AACnD,SAAS,kBAAkB;AAyCpB,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,UAAkB;AAClB,iBAAW,KAAK;AAChB,cAAQ,eAAe,IAAI,QAAQ,SAAS;AAAA,IAC7C;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;",
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.e10ff229bb14",
4
+ "version": "5.4.0-canary.ea38889d4bb7",
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.e10ff229bb14",
49
+ "@tldraw/driver": "5.4.0-canary.ea38889d4bb7",
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.e10ff229bb14",
60
- "@tldraw/utils": "5.4.0-canary.e10ff229bb14",
61
- "tldraw": "5.4.0-canary.e10ff229bb14"
67
+ "@tldraw/mentions": "5.4.0-canary.ea38889d4bb7",
68
+ "@tldraw/utils": "5.4.0-canary.ea38889d4bb7",
69
+ "tldraw": "5.4.0-canary.ea38889d4bb7"
62
70
  },
63
71
  "module": "dist-esm/index.mjs",
64
72
  "source": "src/index.ts",
@@ -1,15 +1,17 @@
1
- import { useLicenseContext, useLicenseFeatureFlag } from 'tldraw'
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(useLicenseContext(), 'commenting')
16
+ return useLicenseFeatureFlag(useMaybeLicenseManager(), 'commenting')
15
17
  }
@@ -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
- /** Called when an emoji is chosen. */
37
- onSelect?(emoji: string): void
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
- tlmenus.deleteOpenMenu(id, editor?.contextId)
83
+ if (!isMultiSelectPick(event)) {
84
+ tlmenus.deleteOpenMenu(id, editor?.contextId)
85
+ }
75
86
  },
76
87
  [onSelect, id, editor]
77
88
  )