@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 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';
@@ -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
  }
@@ -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.b6aebcb84bf1",
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/** 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';
@@ -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
  }
@@ -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.
@@ -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.b6aebcb84bf1",
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/** 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.b6aebcb84bf1",
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.b6aebcb84bf1",
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.b6aebcb84bf1",
60
- "@tldraw/utils": "5.4.0-canary.b6aebcb84bf1",
61
- "tldraw": "5.4.0-canary.b6aebcb84bf1"
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",
@@ -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
  )