@tldraw/mentions 5.3.2 → 5.4.0-canary.02cd0bd3b597

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist-cjs/index.js CHANGED
@@ -35,7 +35,7 @@ var import_mention_extension = require("./mention-extension");
35
35
  var import_mention_suggestion = require("./mention-suggestion");
36
36
  (0, import_utils.registerTldrawLibraryVersion)(
37
37
  "@tldraw/mentions",
38
- "5.3.2",
38
+ "5.4.0-canary.02cd0bd3b597",
39
39
  "cjs"
40
40
  );
41
41
  //# sourceMappingURL=index.js.map
@@ -53,9 +53,7 @@ const MentionList = (0, import_react.forwardRef)(function MentionList2({ members
53
53
  "aria-selected": active,
54
54
  className: active ? "tlui-cmt-mention-list__item tlui-cmt-mention-list__item--active" : "tlui-cmt-mention-list__item",
55
55
  onMouseDown: (e) => e.preventDefault(),
56
- onClick: () => {
57
- if (onSelect) onSelect(member);
58
- },
56
+ onClick: () => onSelect?.(member),
59
57
  children: renderMember ? renderMember(member) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultMemberRow, { member })
60
58
  },
61
59
  member.id
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/mention-list.tsx"],
4
- "sourcesContent": ["import { ReactNode, forwardRef } from 'react'\nimport { useTranslation } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { CommentAuthor } from './comment-author'\n\n/** A person the composer's \\@-mention picker can offer: a {@link CommentAuthor} plus its id and\n * picker-only display fields. @public */\nexport interface MentionMember extends CommentAuthor {\n\tid: string\n\t/** A secondary line under the name \u2014 e.g. an email or handle. Omit for a single-line row. */\n\tsecondary?: string\n\t/** Marks the current user; shown as a \"(You)\" suffix after the name. */\n\tyou?: boolean\n\t/** Hosts may carry extra fields; a custom `renderMember` can read them. */\n\t[key: string]: unknown\n}\n\n/** @public */\nexport interface MentionListProps {\n\t/** Members to choose from \u2014 already resolved for the current query by the host. */\n\tmembers: MentionMember[]\n\t/** Index of the highlighted row, driven by the composer's keyboard navigation. */\n\tactiveIndex?: number\n\t/** Called when a member is chosen (click, or Enter on the active row). */\n\tonSelect?(member: MentionMember): void\n\t/** Shown when no member matches the query. Defaults to a translated \"No matches\". */\n\temptyLabel?: string\n\t/** Override a row's content (inside the selectable button). Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The default row content: avatar, name (with a \"(You)\" marker), and an optional secondary line. */\nfunction DefaultMemberRow({ member }: { member: MentionMember }) {\n\tconst msg = useTranslation()\n\treturn (\n\t\t<>\n\t\t\t<Avatar author={member} />\n\t\t\t<span className=\"tlui-cmt-mention-list__text\">\n\t\t\t\t<span className=\"tlui-cmt-mention-list__name\">\n\t\t\t\t\t{member.name}\n\t\t\t\t\t{member.you && (\n\t\t\t\t\t\t<span className=\"tlui-cmt-mention-list__you\">{`(${msg('comments.mention-you')})`}</span>\n\t\t\t\t\t)}\n\t\t\t\t</span>\n\t\t\t\t{member.secondary && (\n\t\t\t\t\t<span className=\"tlui-cmt-mention-list__secondary\">{member.secondary}</span>\n\t\t\t\t)}\n\t\t\t</span>\n\t\t</>\n\t)\n}\n\n/**\n * The \\@-mention member picker: a popover list of people to mention, shown as the user types after\n * `@`. Presentational \u2014 the composer's suggestion plugin owns resolution, keyboard navigation, and\n * placement, and drives this with the host-resolved `members` and the highlighted `activeIndex`.\n * @public @react\n */\nexport const MentionList = forwardRef<HTMLDivElement, MentionListProps>(function MentionList(\n\t{ members, activeIndex = 0, onSelect, emptyLabel, renderMember },\n\tref\n) {\n\tconst msg = useTranslation()\n\tif (members.length === 0) {\n\t\treturn (\n\t\t\t<div ref={ref} className=\"tlui-cmt-mention-list tlui-cmt-mention-list--empty\">\n\t\t\t\t{emptyLabel ?? msg('comments.mention-no-matches')}\n\t\t\t</div>\n\t\t)\n\t}\n\treturn (\n\t\t<div ref={ref} className=\"tlui-cmt-mention-list\" role=\"listbox\">\n\t\t\t{members.map((member, i) => {\n\t\t\t\tconst active = i === activeIndex\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={member.id}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\taria-selected={active}\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-mention-list__item tlui-cmt-mention-list__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-mention-list__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Keep the composer focused on a row press: it's portaled outside the editor, so a\n\t\t\t\t\t\t// bare mousedown would blur the composer \u2014 which now dismisses the picker \u2014 before\n\t\t\t\t\t\t// the click could select. Selection runs on click; focus stays put.\n\t\t\t\t\t\tonMouseDown={(e) => e.preventDefault()}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tif (onSelect) onSelect(member)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderMember ? renderMember(member) : <DefaultMemberRow member={member} />}\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;AAmCE;AAnCF,mBAAsC;AACtC,oBAA+B;AAC/B,oBAAuB;AA8BvB,SAAS,iBAAiB,EAAE,OAAO,GAA8B;AAChE,QAAM,UAAM,8BAAe;AAC3B,SACC,4EACC;AAAA,gDAAC,wBAAO,QAAQ,QAAQ;AAAA,IACxB,6CAAC,UAAK,WAAU,+BACf;AAAA,mDAAC,UAAK,WAAU,+BACd;AAAA,eAAO;AAAA,QACP,OAAO,OACP,4CAAC,UAAK,WAAU,8BAA8B,cAAI,IAAI,sBAAsB,CAAC,KAAI;AAAA,SAEnF;AAAA,MACC,OAAO,aACP,4CAAC,UAAK,WAAU,oCAAoC,iBAAO,WAAU;AAAA,OAEvE;AAAA,KACD;AAEF;AAQO,MAAM,kBAAc,yBAA6C,SAASA,aAChF,EAAE,SAAS,cAAc,GAAG,UAAU,YAAY,aAAa,GAC/D,KACC;AACD,QAAM,UAAM,8BAAe;AAC3B,MAAI,QAAQ,WAAW,GAAG;AACzB,WACC,4CAAC,SAAI,KAAU,WAAU,sDACvB,wBAAc,IAAI,6BAA6B,GACjD;AAAA,EAEF;AACA,SACC,4CAAC,SAAI,KAAU,WAAU,yBAAwB,MAAK,WACpD,kBAAQ,IAAI,CAAC,QAAQ,MAAM;AAC3B,UAAM,SAAS,MAAM;AACrB,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WACC,SACG,oEACA;AAAA,QAKJ,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM;AACd,cAAI,SAAU,UAAS,MAAM;AAAA,QAC9B;AAAA,QAEC,yBAAe,aAAa,MAAM,IAAI,4CAAC,oBAAiB,QAAgB;AAAA;AAAA,MAjBpE,OAAO;AAAA,IAkBb;AAAA,EAEF,CAAC,GACF;AAEF,CAAC;",
4
+ "sourcesContent": ["import { ReactNode, forwardRef } from 'react'\nimport { useTranslation } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { CommentAuthor } from './comment-author'\n\n/** A person the composer's \\@-mention picker can offer: a {@link CommentAuthor} plus its id and\n * picker-only display fields. @public */\nexport interface MentionMember extends CommentAuthor {\n\tid: string\n\t/** A secondary line under the name \u2014 e.g. an email or handle. Omit for a single-line row. */\n\tsecondary?: string\n\t/** Marks the current user; shown as a \"(You)\" suffix after the name. */\n\tyou?: boolean\n\t/** Hosts may carry extra fields; a custom `renderMember` can read them. */\n\t[key: string]: unknown\n}\n\n/** @public */\nexport interface MentionListProps {\n\t/** Members to choose from \u2014 already resolved for the current query by the host. */\n\tmembers: MentionMember[]\n\t/** Index of the highlighted row, driven by the composer's keyboard navigation. */\n\tactiveIndex?: number\n\t/** Called when a member is chosen (click, or Enter on the active row). */\n\tonSelect?(member: MentionMember): void\n\t/** Shown when no member matches the query. Defaults to a translated \"No matches\". */\n\temptyLabel?: string\n\t/** Override a row's content (inside the selectable button). Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The default row content: avatar, name (with a \"(You)\" marker), and an optional secondary line. */\nfunction DefaultMemberRow({ member }: { member: MentionMember }) {\n\tconst msg = useTranslation()\n\treturn (\n\t\t<>\n\t\t\t<Avatar author={member} />\n\t\t\t<span className=\"tlui-cmt-mention-list__text\">\n\t\t\t\t<span className=\"tlui-cmt-mention-list__name\">\n\t\t\t\t\t{member.name}\n\t\t\t\t\t{member.you && (\n\t\t\t\t\t\t<span className=\"tlui-cmt-mention-list__you\">{`(${msg('comments.mention-you')})`}</span>\n\t\t\t\t\t)}\n\t\t\t\t</span>\n\t\t\t\t{member.secondary && (\n\t\t\t\t\t<span className=\"tlui-cmt-mention-list__secondary\">{member.secondary}</span>\n\t\t\t\t)}\n\t\t\t</span>\n\t\t</>\n\t)\n}\n\n/**\n * The \\@-mention member picker: a popover list of people to mention, shown as the user types after\n * `@`. Presentational \u2014 the composer's suggestion plugin owns resolution, keyboard navigation, and\n * placement, and drives this with the host-resolved `members` and the highlighted `activeIndex`.\n * @public @react\n */\nexport const MentionList = forwardRef<HTMLDivElement, MentionListProps>(function MentionList(\n\t{ members, activeIndex = 0, onSelect, emptyLabel, renderMember },\n\tref\n) {\n\tconst msg = useTranslation()\n\tif (members.length === 0) {\n\t\treturn (\n\t\t\t<div ref={ref} className=\"tlui-cmt-mention-list tlui-cmt-mention-list--empty\">\n\t\t\t\t{emptyLabel ?? msg('comments.mention-no-matches')}\n\t\t\t</div>\n\t\t)\n\t}\n\treturn (\n\t\t<div ref={ref} className=\"tlui-cmt-mention-list\" role=\"listbox\">\n\t\t\t{members.map((member, i) => {\n\t\t\t\tconst active = i === activeIndex\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={member.id}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\taria-selected={active}\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-mention-list__item tlui-cmt-mention-list__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-mention-list__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Keep the composer focused on a row press: it's portaled outside the editor, so a\n\t\t\t\t\t\t// bare mousedown would blur the composer \u2014 which now dismisses the picker \u2014 before\n\t\t\t\t\t\t// the click could select. Selection runs on click; focus stays put.\n\t\t\t\t\t\tonMouseDown={(e) => e.preventDefault()}\n\t\t\t\t\t\tonClick={() => onSelect?.(member)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderMember ? renderMember(member) : <DefaultMemberRow member={member} />}\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;AAmCE;AAnCF,mBAAsC;AACtC,oBAA+B;AAC/B,oBAAuB;AA8BvB,SAAS,iBAAiB,EAAE,OAAO,GAA8B;AAChE,QAAM,UAAM,8BAAe;AAC3B,SACC,4EACC;AAAA,gDAAC,wBAAO,QAAQ,QAAQ;AAAA,IACxB,6CAAC,UAAK,WAAU,+BACf;AAAA,mDAAC,UAAK,WAAU,+BACd;AAAA,eAAO;AAAA,QACP,OAAO,OACP,4CAAC,UAAK,WAAU,8BAA8B,cAAI,IAAI,sBAAsB,CAAC,KAAI;AAAA,SAEnF;AAAA,MACC,OAAO,aACP,4CAAC,UAAK,WAAU,oCAAoC,iBAAO,WAAU;AAAA,OAEvE;AAAA,KACD;AAEF;AAQO,MAAM,kBAAc,yBAA6C,SAASA,aAChF,EAAE,SAAS,cAAc,GAAG,UAAU,YAAY,aAAa,GAC/D,KACC;AACD,QAAM,UAAM,8BAAe;AAC3B,MAAI,QAAQ,WAAW,GAAG;AACzB,WACC,4CAAC,SAAI,KAAU,WAAU,sDACvB,wBAAc,IAAI,6BAA6B,GACjD;AAAA,EAEF;AACA,SACC,4CAAC,SAAI,KAAU,WAAU,yBAAwB,MAAK,WACpD,kBAAQ,IAAI,CAAC,QAAQ,MAAM;AAC3B,UAAM,SAAS,MAAM;AACrB,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WACC,SACG,oEACA;AAAA,QAKJ,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM,WAAW,MAAM;AAAA,QAE/B,yBAAe,aAAa,MAAM,IAAI,4CAAC,oBAAiB,QAAgB;AAAA;AAAA,MAfpE,OAAO;AAAA,IAgBb;AAAA,EAEF,CAAC,GACF;AAEF,CAAC;",
6
6
  "names": ["MentionList"]
7
7
  }
@@ -129,14 +129,15 @@ function createMentionSuggestion(getSuggestions, options = {}) {
129
129
  if (container) container.style.display = "none";
130
130
  mentionPickerOpen.set(false);
131
131
  };
132
+ const popupProps = (props) => ({
133
+ items: props.items,
134
+ command: props.command,
135
+ renderMember: options.renderMember
136
+ });
132
137
  return {
133
138
  onStart: (props) => {
134
139
  renderer = new import_react.ReactRenderer(MentionPopup, {
135
- props: {
136
- items: props.items,
137
- command: props.command,
138
- renderMember: options.renderMember
139
- },
140
+ props: popupProps(props),
140
141
  editor: props.editor
141
142
  });
142
143
  editorEl = props.editor.view.dom;
@@ -150,12 +151,7 @@ function createMentionSuggestion(getSuggestions, options = {}) {
150
151
  mentionPickerOpen.set(true);
151
152
  },
152
153
  onUpdate: (props) => {
153
- if (renderer)
154
- renderer.updateProps({
155
- items: props.items,
156
- command: props.command,
157
- renderMember: options.renderMember
158
- });
154
+ renderer?.updateProps(popupProps(props));
159
155
  if (container) container.style.display = "";
160
156
  mentionPickerOpen.set(true);
161
157
  place();
@@ -175,14 +171,13 @@ function createMentionSuggestion(getSuggestions, options = {}) {
175
171
  }
176
172
  return true;
177
173
  }
178
- if (renderer && renderer.ref) return renderer.ref.onKeyDown(props);
179
- return false;
174
+ return renderer?.ref?.onKeyDown(props) ?? false;
180
175
  },
181
176
  onExit: () => {
182
177
  stopFollowing();
183
- if (editorEl) editorEl.removeEventListener("blur", hide);
184
- if (container) container.remove();
185
- if (renderer) renderer.destroy();
178
+ editorEl?.removeEventListener("blur", hide);
179
+ container?.remove();
180
+ renderer?.destroy();
186
181
  renderer = null;
187
182
  container = null;
188
183
  mentionPickerOpen.set(false);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/mention-suggestion.tsx"],
4
- "sourcesContent": ["import type { MentionNodeAttrs } from '@tiptap/extension-mention'\nimport { ReactRenderer } from '@tiptap/react'\nimport type { SuggestionKeyDownProps, SuggestionOptions } from '@tiptap/suggestion'\nimport { type ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { type Editor as TldrawEditor, atom, react, usePassThroughWheelEvents } from 'tldraw'\nimport { MentionList, MentionMember } from './mention-list'\n\n/** The handle the suggestion plugin drives \u2014 it forwards navigation keys into the popup. */\nexport interface MentionPopupHandle {\n\tonKeyDown(props: SuggestionKeyDownProps): boolean\n}\n\ninterface MentionPopupProps {\n\titems: MentionMember[]\n\tcommand(attrs: MentionNodeAttrs): void\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The live @-picker popup: owns the highlighted index and keyboard, renders the presentational list. */\nconst MentionPopup = forwardRef<MentionPopupHandle, MentionPopupProps>(function MentionPopup(\n\t{ items, command, renderMember },\n\tref\n) {\n\tconst [activeIndex, setActiveIndex] = useState(0)\n\t// A wheel over the popup drives the canvas beneath it, the same pass-through every tldraw panel gets;\n\t// the hook leaves the list alone while it scrolls its own overflow. The suggestion plugin builds the\n\t// popup imperatively, but `ReactRenderer` portals this into the composer's tree, so context reaches it.\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(listRef)\n\t// A new query yields new items; reset the highlight to the top during render \u2014 not in an effect,\n\t// which would leave a frame where `activeIndex` still points past a shrunk list and Enter selects\n\t// its (now out-of-range, undefined) item, swallowing the key without inserting a mention.\n\tconst [prevItems, setPrevItems] = useState(items)\n\tif (items !== prevItems) {\n\t\tsetPrevItems(items)\n\t\tsetActiveIndex(0)\n\t}\n\n\tconst select = (member: MentionMember | undefined) => {\n\t\tif (member) command({ id: member.id, label: member.name })\n\t}\n\n\tuseImperativeHandle(ref, () => ({\n\t\tonKeyDown: ({ event }) => {\n\t\t\tif (items.length === 0) return false\n\t\t\tif (event.key === 'ArrowUp') {\n\t\t\t\tsetActiveIndex((i) => (i + items.length - 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (event.key === 'ArrowDown') {\n\t\t\t\tsetActiveIndex((i) => (i + 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Enter and Tab both complete the highlighted member (falling back to the top match so a\n\t\t\t// stale index never selects `undefined`). The empty-roster case is handled a level up, in\n\t\t\t// the suggestion's onKeyDown, which cancels the picker.\n\t\t\tif (event.key === 'Enter' || event.key === 'Tab') {\n\t\t\t\tselect(items[activeIndex] ?? items[0])\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}))\n\n\treturn (\n\t\t<MentionList\n\t\t\tref={listRef}\n\t\t\tmembers={items}\n\t\t\tactiveIndex={activeIndex}\n\t\t\tonSelect={select}\n\t\t\trenderMember={renderMember}\n\t\t/>\n\t)\n})\n\nconst MAX_SUGGESTIONS = 8\n\n/** Members whose name contains the query (case-insensitive), capped to the popup's length.\n * @public */\nexport function filterMentionMembers(members: MentionMember[], query: string): MentionMember[] {\n\tconst q = query.toLowerCase()\n\treturn members.filter((m) => m.name.toLowerCase().includes(q)).slice(0, MAX_SUGGESTIONS)\n}\n\n// Deliberately NOT tldraw's open-menu registry: registering there mounts MenuClickCapture, which\n// makes the canvas inert. The picker is an inline autocomplete, not a modal \u2014 the canvas must stay\n// pannable beneath it \u2014 so track \"open\" ourselves.\nconst mentionPickerOpen = atom('isMentionPickerOpen', false)\n\n/**\n * Whether the \\@-mention picker is currently showing. Host dismissal (Escape, outside-click) checks\n * this so it can defer to the picker instead of tearing down the composer or thread beneath it.\n * @public\n */\nexport function isMentionPickerOpen(): boolean {\n\treturn mentionPickerOpen.get()\n}\n\n/** @public */\nexport interface MentionSuggestionOptions {\n\t/** Override a member row's content in the picker. Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n\t/**\n\t * The tldraw editor the composer lives in. When provided, the popup re-anchors reactively as the\n\t * canvas camera moves (the composer rides it) instead of polling every frame. Omit off-canvas.\n\t */\n\teditor?: TldrawEditor | null\n}\n\n/**\n * Build the TipTap `suggestion` config for the \\@-picker. `getSuggestions(query)` is the host's\n * resolver \u2014 the SDK owns neither the roster nor the filtering. The plugin runs outside React, so\n * `render` mounts `MentionPopup` via a `ReactRenderer` and forwards navigation keys to it.\n * @public\n */\nexport function createMentionSuggestion(\n\tgetSuggestions: (query: string) => MentionMember[] | Promise<MentionMember[]>,\n\toptions: MentionSuggestionOptions = {}\n): Omit<SuggestionOptions<MentionMember, MentionNodeAttrs>, 'editor'> {\n\treturn {\n\t\tchar: '@',\n\t\titems: ({ query }) => getSuggestions(query),\n\t\trender: () => {\n\t\t\tlet renderer: ReactRenderer<MentionPopupHandle, MentionPopupProps> | null = null\n\t\t\tlet container: HTMLElement | null = null\n\t\t\tlet editorEl: HTMLElement | null = null\n\t\t\tlet stopCameraReaction: (() => void) | null = null\n\t\t\t// The composer field's top-left in page space, plus the popup's screen width. Captured on a\n\t\t\t// fresh read so camera moves can re-derive the popup's screen position from the page anchor\n\t\t\t// (see reposition) rather than the field's DOM rect.\n\t\t\tlet anchorPage: { x: number; y: number } | null = null\n\t\t\tlet popupWidth = 0\n\n\t\t\tconst applyScreen = (left: number, top: number, width: number) => {\n\t\t\t\tif (!container) return\n\t\t\t\tcontainer.style.left = `${left}px`\n\t\t\t\tcontainer.style.top = `${top + 4}px`\n\t\t\t\tcontainer.style.width = `${width}px`\n\t\t\t}\n\n\t\t\t// Fresh placement: read the field's real screen rect, position the popup flush under it (not\n\t\t\t// the caret, matching its width), and remember the field's page-space anchor for reposition.\n\t\t\tconst place = () => {\n\t\t\t\tif (!container || !editorEl || container.style.display === 'none') return\n\t\t\t\tconst field = editorEl.closest('.tlui-cmt-composer__field') ?? editorEl\n\t\t\t\tconst rect = field.getBoundingClientRect()\n\t\t\t\tpopupWidth = rect.width\n\t\t\t\tanchorPage = options.editor?.screenToPage({ x: rect.left, y: rect.bottom }) ?? null\n\t\t\t\tapplyScreen(rect.left, rect.bottom, rect.width)\n\t\t\t}\n\n\t\t\t// Re-derived from the remembered page anchor \u2014 pure camera math, always current. Reading the field's\n\t\t\t// DOM rect would lag a frame: the composer re-positions on a React commit, after the camera reaction.\n\t\t\tconst reposition = () => {\n\t\t\t\tif (!anchorPage || !options.editor) return\n\t\t\t\tconst s = options.editor.pageToScreen(anchorPage)\n\t\t\t\tapplyScreen(s.x, s.y, popupWidth)\n\t\t\t}\n\n\t\t\t// The popup is `position: fixed`, but a canvas composer rides the camera, which moves it with no\n\t\t\t// scroll/resize event to hook. Re-anchor from a tldraw reaction rather than polling every frame,\n\t\t\t// plus on window scroll/resize for the off-canvas case.\n\t\t\tconst startFollowing = () => {\n\t\t\t\twindow.addEventListener('scroll', place, true)\n\t\t\t\twindow.addEventListener('resize', place)\n\t\t\t\tif (options.editor) {\n\t\t\t\t\tstopCameraReaction = react('anchor mention popup to camera', () => {\n\t\t\t\t\t\toptions.editor!.getCamera() // track the camera so this re-runs as it moves\n\t\t\t\t\t\treposition()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst stopFollowing = () => {\n\t\t\t\twindow.removeEventListener('scroll', place, true)\n\t\t\t\twindow.removeEventListener('resize', place)\n\t\t\t\tstopCameraReaction?.()\n\t\t\t\tstopCameraReaction = null\n\t\t\t\tanchorPage = null\n\t\t\t}\n\n\t\t\t// Dismiss the roster on Escape or blur, clearing the open flag so isMentionPickerOpen() stays\n\t\t\t// accurate and the thread's own dismissal can take over. Typing re-shows it via onUpdate.\n\t\t\tconst hide = () => {\n\t\t\t\tif (container) container.style.display = 'none'\n\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tonStart: (props) => {\n\t\t\t\t\trenderer = new ReactRenderer(MentionPopup, {\n\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\titems: props.items,\n\t\t\t\t\t\t\tcommand: props.command,\n\t\t\t\t\t\t\trenderMember: options.renderMember,\n\t\t\t\t\t\t},\n\t\t\t\t\t\teditor: props.editor,\n\t\t\t\t\t})\n\t\t\t\t\teditorEl = props.editor.view.dom as HTMLElement\n\t\t\t\t\t// The TipTap suggestion has no blur handling, so without this the picker would stay\n\t\t\t\t\t// \"open\" (and the thread would defer its Escape to it) after focus moved away from the\n\t\t\t\t\t// composer \u2014 leaving Escape a no-op and the roster stuck on screen.\n\t\t\t\t\teditorEl.addEventListener('blur', hide)\n\t\t\t\t\tcontainer = document.createElement('div')\n\t\t\t\t\tcontainer.className = 'tlui-cmt-mention-popup'\n\t\t\t\t\tcontainer.appendChild(renderer.element)\n\t\t\t\t\t// Mounted inside the tldraw container so the popup inherits the theme variables and the pass-through\n\t\t\t\t\t// hooks can find the canvas. Outside one, fall back to the body \u2014 the picker still works.\n\t\t\t\t\t;(editorEl.closest('.tl-container') ?? document.body).appendChild(container)\n\t\t\t\t\tplace()\n\t\t\t\t\tstartFollowing()\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t},\n\t\t\t\tonUpdate: (props) => {\n\t\t\t\t\tif (renderer)\n\t\t\t\t\t\trenderer.updateProps({\n\t\t\t\t\t\t\titems: props.items,\n\t\t\t\t\t\t\tcommand: props.command,\n\t\t\t\t\t\t\trenderMember: options.renderMember,\n\t\t\t\t\t\t})\n\t\t\t\t\t// Typing after an Escape re-shows the roster.\n\t\t\t\t\tif (container) container.style.display = ''\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t\tplace()\n\t\t\t\t},\n\t\t\t\tonKeyDown: (props) => {\n\t\t\t\t\t// Once the roster is hidden, the suggestion stays active but this handler goes inert \u2014 keys pass\n\t\t\t\t\t// through so a second Escape closes the composer. Typing re-shows the roster via onUpdate.\n\t\t\t\t\tif (!isMentionPickerOpen()) return false\n\t\t\t\t\tif (props.event.key === 'Escape') {\n\t\t\t\t\t\t// Dismiss only the roster: hide it and stop the key so the composer/thread beneath\n\t\t\t\t\t\t// doesn't also treat Escape as \"abandon\".\n\t\t\t\t\t\thide()\n\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (props.event.key === 'Enter' || props.event.key === 'Tab') {\n\t\t\t\t\t\t// Complete the highlighted member if there is one to complete; if the roster is empty\n\t\t\t\t\t\t// there's nothing to pick, so cancel the picker and swallow the key \u2014 the composer\n\t\t\t\t\t\t// beneath neither submits (Enter) nor moves focus / indents (Tab).\n\t\t\t\t\t\tconst completed = renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t\t\tif (!completed) {\n\t\t\t\t\t\t\thide()\n\t\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (renderer && renderer.ref) return renderer.ref.onKeyDown(props)\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t\tonExit: () => {\n\t\t\t\t\tstopFollowing()\n\t\t\t\t\tif (editorEl) editorEl.removeEventListener('blur', hide)\n\t\t\t\t\tif (container) container.remove()\n\t\t\t\t\tif (renderer) renderer.destroy()\n\t\t\t\t\trenderer = null\n\t\t\t\t\tcontainer = null\n\t\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MentionNodeAttrs } from '@tiptap/extension-mention'\nimport { ReactRenderer } from '@tiptap/react'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport { type ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { type Editor as TldrawEditor, atom, react, usePassThroughWheelEvents } from 'tldraw'\nimport { MentionList, MentionMember } from './mention-list'\n\n/** The handle the suggestion plugin drives \u2014 it forwards navigation keys into the popup. */\ninterface MentionPopupHandle {\n\tonKeyDown(props: SuggestionKeyDownProps): boolean\n}\n\ninterface MentionPopupProps {\n\titems: MentionMember[]\n\tcommand(attrs: MentionNodeAttrs): void\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The live @-picker popup: owns the highlighted index and keyboard, renders the presentational list. */\nconst MentionPopup = forwardRef<MentionPopupHandle, MentionPopupProps>(function MentionPopup(\n\t{ items, command, renderMember },\n\tref\n) {\n\tconst [activeIndex, setActiveIndex] = useState(0)\n\t// A wheel over the popup drives the canvas beneath it, the same pass-through every tldraw panel gets;\n\t// the hook leaves the list alone while it scrolls its own overflow. The suggestion plugin builds the\n\t// popup imperatively, but `ReactRenderer` portals this into the composer's tree, so context reaches it.\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(listRef)\n\t// A new query yields new items; reset the highlight to the top during render \u2014 not in an effect,\n\t// which would leave a frame where `activeIndex` still points past a shrunk list and Enter selects\n\t// its (now out-of-range, undefined) item, swallowing the key without inserting a mention.\n\tconst [prevItems, setPrevItems] = useState(items)\n\tif (items !== prevItems) {\n\t\tsetPrevItems(items)\n\t\tsetActiveIndex(0)\n\t}\n\n\tconst select = (member: MentionMember | undefined) => {\n\t\tif (member) command({ id: member.id, label: member.name })\n\t}\n\n\tuseImperativeHandle(ref, () => ({\n\t\tonKeyDown: ({ event }) => {\n\t\t\tif (items.length === 0) return false\n\t\t\tif (event.key === 'ArrowUp') {\n\t\t\t\tsetActiveIndex((i) => (i + items.length - 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (event.key === 'ArrowDown') {\n\t\t\t\tsetActiveIndex((i) => (i + 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Enter and Tab both complete the highlighted member (falling back to the top match so a\n\t\t\t// stale index never selects `undefined`). The empty-roster case is handled a level up, in\n\t\t\t// the suggestion's onKeyDown, which cancels the picker.\n\t\t\tif (event.key === 'Enter' || event.key === 'Tab') {\n\t\t\t\tselect(items[activeIndex] ?? items[0])\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}))\n\n\treturn (\n\t\t<MentionList\n\t\t\tref={listRef}\n\t\t\tmembers={items}\n\t\t\tactiveIndex={activeIndex}\n\t\t\tonSelect={select}\n\t\t\trenderMember={renderMember}\n\t\t/>\n\t)\n})\n\nconst MAX_SUGGESTIONS = 8\n\n/** Members whose name contains the query (case-insensitive), capped to the popup's length.\n * @public */\nexport function filterMentionMembers(members: MentionMember[], query: string): MentionMember[] {\n\tconst q = query.toLowerCase()\n\treturn members.filter((m) => m.name.toLowerCase().includes(q)).slice(0, MAX_SUGGESTIONS)\n}\n\n// Deliberately NOT tldraw's open-menu registry: registering there mounts MenuClickCapture, which\n// makes the canvas inert. The picker is an inline autocomplete, not a modal \u2014 the canvas must stay\n// pannable beneath it \u2014 so track \"open\" ourselves.\nconst mentionPickerOpen = atom('isMentionPickerOpen', false)\n\n/**\n * Whether the \\@-mention picker is currently showing. Host dismissal (Escape, outside-click) checks\n * this so it can defer to the picker instead of tearing down the composer or thread beneath it.\n * @public\n */\nexport function isMentionPickerOpen(): boolean {\n\treturn mentionPickerOpen.get()\n}\n\n/** @public */\nexport interface MentionSuggestionOptions {\n\t/** Override a member row's content in the picker. Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n\t/**\n\t * The tldraw editor the composer lives in. When provided, the popup re-anchors reactively as the\n\t * canvas camera moves (the composer rides it) instead of polling every frame. Omit off-canvas.\n\t */\n\teditor?: TldrawEditor | null\n}\n\n/**\n * Build the TipTap `suggestion` config for the \\@-picker. `getSuggestions(query)` is the host's\n * resolver \u2014 the SDK owns neither the roster nor the filtering. The plugin runs outside React, so\n * `render` mounts `MentionPopup` via a `ReactRenderer` and forwards navigation keys to it.\n * @public\n */\nexport function createMentionSuggestion(\n\tgetSuggestions: (query: string) => MentionMember[] | Promise<MentionMember[]>,\n\toptions: MentionSuggestionOptions = {}\n): Omit<SuggestionOptions<MentionMember, MentionNodeAttrs>, 'editor'> {\n\treturn {\n\t\tchar: '@',\n\t\titems: ({ query }) => getSuggestions(query),\n\t\trender: () => {\n\t\t\tlet renderer: ReactRenderer<MentionPopupHandle, MentionPopupProps> | null = null\n\t\t\tlet container: HTMLElement | null = null\n\t\t\tlet editorEl: HTMLElement | null = null\n\t\t\tlet stopCameraReaction: (() => void) | null = null\n\t\t\t// The composer field's top-left in page space, plus the popup's screen width. Captured on a\n\t\t\t// fresh read so camera moves can re-derive the popup's screen position from the page anchor\n\t\t\t// (see reposition) rather than the field's DOM rect.\n\t\t\tlet anchorPage: { x: number; y: number } | null = null\n\t\t\tlet popupWidth = 0\n\n\t\t\tconst applyScreen = (left: number, top: number, width: number) => {\n\t\t\t\tif (!container) return\n\t\t\t\tcontainer.style.left = `${left}px`\n\t\t\t\tcontainer.style.top = `${top + 4}px`\n\t\t\t\tcontainer.style.width = `${width}px`\n\t\t\t}\n\n\t\t\t// Fresh placement: read the field's real screen rect, position the popup flush under it (not\n\t\t\t// the caret, matching its width), and remember the field's page-space anchor for reposition.\n\t\t\tconst place = () => {\n\t\t\t\tif (!container || !editorEl || container.style.display === 'none') return\n\t\t\t\tconst field = editorEl.closest('.tlui-cmt-composer__field') ?? editorEl\n\t\t\t\tconst rect = field.getBoundingClientRect()\n\t\t\t\tpopupWidth = rect.width\n\t\t\t\tanchorPage = options.editor?.screenToPage({ x: rect.left, y: rect.bottom }) ?? null\n\t\t\t\tapplyScreen(rect.left, rect.bottom, rect.width)\n\t\t\t}\n\n\t\t\t// Re-derived from the remembered page anchor \u2014 pure camera math, always current. Reading the field's\n\t\t\t// DOM rect would lag a frame: the composer re-positions on a React commit, after the camera reaction.\n\t\t\tconst reposition = () => {\n\t\t\t\tif (!anchorPage || !options.editor) return\n\t\t\t\tconst s = options.editor.pageToScreen(anchorPage)\n\t\t\t\tapplyScreen(s.x, s.y, popupWidth)\n\t\t\t}\n\n\t\t\t// The popup is `position: fixed`, but a canvas composer rides the camera, which moves it with no\n\t\t\t// scroll/resize event to hook. Re-anchor from a tldraw reaction rather than polling every frame,\n\t\t\t// plus on window scroll/resize for the off-canvas case.\n\t\t\tconst startFollowing = () => {\n\t\t\t\twindow.addEventListener('scroll', place, true)\n\t\t\t\twindow.addEventListener('resize', place)\n\t\t\t\tif (options.editor) {\n\t\t\t\t\tstopCameraReaction = react('anchor mention popup to camera', () => {\n\t\t\t\t\t\toptions.editor!.getCamera() // track the camera so this re-runs as it moves\n\t\t\t\t\t\treposition()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst stopFollowing = () => {\n\t\t\t\twindow.removeEventListener('scroll', place, true)\n\t\t\t\twindow.removeEventListener('resize', place)\n\t\t\t\tstopCameraReaction?.()\n\t\t\t\tstopCameraReaction = null\n\t\t\t\tanchorPage = null\n\t\t\t}\n\n\t\t\t// Dismiss the roster on Escape or blur, clearing the open flag so isMentionPickerOpen() stays\n\t\t\t// accurate and the thread's own dismissal can take over. Typing re-shows it via onUpdate.\n\t\t\tconst hide = () => {\n\t\t\t\tif (container) container.style.display = 'none'\n\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t}\n\n\t\t\tconst popupProps = (\n\t\t\t\tprops: SuggestionProps<MentionMember, MentionNodeAttrs>\n\t\t\t): MentionPopupProps => ({\n\t\t\t\titems: props.items,\n\t\t\t\tcommand: props.command,\n\t\t\t\trenderMember: options.renderMember,\n\t\t\t})\n\n\t\t\treturn {\n\t\t\t\tonStart: (props) => {\n\t\t\t\t\trenderer = new ReactRenderer(MentionPopup, {\n\t\t\t\t\t\tprops: popupProps(props),\n\t\t\t\t\t\teditor: props.editor,\n\t\t\t\t\t})\n\t\t\t\t\teditorEl = props.editor.view.dom as HTMLElement\n\t\t\t\t\t// The TipTap suggestion has no blur handling, so without this the picker would stay\n\t\t\t\t\t// \"open\" (and the thread would defer its Escape to it) after focus moved away from the\n\t\t\t\t\t// composer \u2014 leaving Escape a no-op and the roster stuck on screen.\n\t\t\t\t\teditorEl.addEventListener('blur', hide)\n\t\t\t\t\tcontainer = document.createElement('div')\n\t\t\t\t\tcontainer.className = 'tlui-cmt-mention-popup'\n\t\t\t\t\tcontainer.appendChild(renderer.element)\n\t\t\t\t\t// Mounted inside the tldraw container so the popup inherits the theme variables and the pass-through\n\t\t\t\t\t// hooks can find the canvas. Outside one, fall back to the body \u2014 the picker still works.\n\t\t\t\t\t;(editorEl.closest('.tl-container') ?? document.body).appendChild(container)\n\t\t\t\t\tplace()\n\t\t\t\t\tstartFollowing()\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t},\n\t\t\t\tonUpdate: (props) => {\n\t\t\t\t\trenderer?.updateProps(popupProps(props))\n\t\t\t\t\t// Typing after an Escape re-shows the roster.\n\t\t\t\t\tif (container) container.style.display = ''\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t\tplace()\n\t\t\t\t},\n\t\t\t\tonKeyDown: (props) => {\n\t\t\t\t\t// Once the roster is hidden, the suggestion stays active but this handler goes inert \u2014 keys pass\n\t\t\t\t\t// through so a second Escape closes the composer. Typing re-shows the roster via onUpdate.\n\t\t\t\t\tif (!isMentionPickerOpen()) return false\n\t\t\t\t\tif (props.event.key === 'Escape') {\n\t\t\t\t\t\t// Dismiss only the roster: hide it and stop the key so the composer/thread beneath\n\t\t\t\t\t\t// doesn't also treat Escape as \"abandon\".\n\t\t\t\t\t\thide()\n\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (props.event.key === 'Enter' || props.event.key === 'Tab') {\n\t\t\t\t\t\t// Complete the highlighted member if there is one to complete; if the roster is empty\n\t\t\t\t\t\t// there's nothing to pick, so cancel the picker and swallow the key \u2014 the composer\n\t\t\t\t\t\t// beneath neither submits (Enter) nor moves focus / indents (Tab).\n\t\t\t\t\t\tconst completed = renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t\t\tif (!completed) {\n\t\t\t\t\t\t\thide()\n\t\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\treturn renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t},\n\t\t\t\tonExit: () => {\n\t\t\t\t\tstopFollowing()\n\t\t\t\t\teditorEl?.removeEventListener('blur', hide)\n\t\t\t\t\tcontainer?.remove()\n\t\t\t\t\trenderer?.destroy()\n\t\t\t\t\trenderer = null\n\t\t\t\t\tcontainer = null\n\t\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_react", "MentionPopup"]
7
7
  }
@@ -10,7 +10,7 @@ import {
10
10
  } from "./mention-suggestion.mjs";
11
11
  registerTldrawLibraryVersion(
12
12
  "@tldraw/mentions",
13
- "5.3.2",
13
+ "5.4.0-canary.02cd0bd3b597",
14
14
  "esm"
15
15
  );
16
16
  export {
@@ -30,9 +30,7 @@ const MentionList = forwardRef(function MentionList2({ members, activeIndex = 0,
30
30
  "aria-selected": active,
31
31
  className: active ? "tlui-cmt-mention-list__item tlui-cmt-mention-list__item--active" : "tlui-cmt-mention-list__item",
32
32
  onMouseDown: (e) => e.preventDefault(),
33
- onClick: () => {
34
- if (onSelect) onSelect(member);
35
- },
33
+ onClick: () => onSelect?.(member),
36
34
  children: renderMember ? renderMember(member) : /* @__PURE__ */ jsx(DefaultMemberRow, { member })
37
35
  },
38
36
  member.id
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/mention-list.tsx"],
4
- "sourcesContent": ["import { ReactNode, forwardRef } from 'react'\nimport { useTranslation } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { CommentAuthor } from './comment-author'\n\n/** A person the composer's \\@-mention picker can offer: a {@link CommentAuthor} plus its id and\n * picker-only display fields. @public */\nexport interface MentionMember extends CommentAuthor {\n\tid: string\n\t/** A secondary line under the name \u2014 e.g. an email or handle. Omit for a single-line row. */\n\tsecondary?: string\n\t/** Marks the current user; shown as a \"(You)\" suffix after the name. */\n\tyou?: boolean\n\t/** Hosts may carry extra fields; a custom `renderMember` can read them. */\n\t[key: string]: unknown\n}\n\n/** @public */\nexport interface MentionListProps {\n\t/** Members to choose from \u2014 already resolved for the current query by the host. */\n\tmembers: MentionMember[]\n\t/** Index of the highlighted row, driven by the composer's keyboard navigation. */\n\tactiveIndex?: number\n\t/** Called when a member is chosen (click, or Enter on the active row). */\n\tonSelect?(member: MentionMember): void\n\t/** Shown when no member matches the query. Defaults to a translated \"No matches\". */\n\temptyLabel?: string\n\t/** Override a row's content (inside the selectable button). Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The default row content: avatar, name (with a \"(You)\" marker), and an optional secondary line. */\nfunction DefaultMemberRow({ member }: { member: MentionMember }) {\n\tconst msg = useTranslation()\n\treturn (\n\t\t<>\n\t\t\t<Avatar author={member} />\n\t\t\t<span className=\"tlui-cmt-mention-list__text\">\n\t\t\t\t<span className=\"tlui-cmt-mention-list__name\">\n\t\t\t\t\t{member.name}\n\t\t\t\t\t{member.you && (\n\t\t\t\t\t\t<span className=\"tlui-cmt-mention-list__you\">{`(${msg('comments.mention-you')})`}</span>\n\t\t\t\t\t)}\n\t\t\t\t</span>\n\t\t\t\t{member.secondary && (\n\t\t\t\t\t<span className=\"tlui-cmt-mention-list__secondary\">{member.secondary}</span>\n\t\t\t\t)}\n\t\t\t</span>\n\t\t</>\n\t)\n}\n\n/**\n * The \\@-mention member picker: a popover list of people to mention, shown as the user types after\n * `@`. Presentational \u2014 the composer's suggestion plugin owns resolution, keyboard navigation, and\n * placement, and drives this with the host-resolved `members` and the highlighted `activeIndex`.\n * @public @react\n */\nexport const MentionList = forwardRef<HTMLDivElement, MentionListProps>(function MentionList(\n\t{ members, activeIndex = 0, onSelect, emptyLabel, renderMember },\n\tref\n) {\n\tconst msg = useTranslation()\n\tif (members.length === 0) {\n\t\treturn (\n\t\t\t<div ref={ref} className=\"tlui-cmt-mention-list tlui-cmt-mention-list--empty\">\n\t\t\t\t{emptyLabel ?? msg('comments.mention-no-matches')}\n\t\t\t</div>\n\t\t)\n\t}\n\treturn (\n\t\t<div ref={ref} className=\"tlui-cmt-mention-list\" role=\"listbox\">\n\t\t\t{members.map((member, i) => {\n\t\t\t\tconst active = i === activeIndex\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={member.id}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\taria-selected={active}\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-mention-list__item tlui-cmt-mention-list__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-mention-list__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Keep the composer focused on a row press: it's portaled outside the editor, so a\n\t\t\t\t\t\t// bare mousedown would blur the composer \u2014 which now dismisses the picker \u2014 before\n\t\t\t\t\t\t// the click could select. Selection runs on click; focus stays put.\n\t\t\t\t\t\tonMouseDown={(e) => e.preventDefault()}\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\tif (onSelect) onSelect(member)\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderMember ? renderMember(member) : <DefaultMemberRow member={member} />}\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": "AAmCE,mBACC,KAEC,YAHF;AAnCF,SAAoB,kBAAkB;AACtC,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AA8BvB,SAAS,iBAAiB,EAAE,OAAO,GAA8B;AAChE,QAAM,MAAM,eAAe;AAC3B,SACC,iCACC;AAAA,wBAAC,UAAO,QAAQ,QAAQ;AAAA,IACxB,qBAAC,UAAK,WAAU,+BACf;AAAA,2BAAC,UAAK,WAAU,+BACd;AAAA,eAAO;AAAA,QACP,OAAO,OACP,oBAAC,UAAK,WAAU,8BAA8B,cAAI,IAAI,sBAAsB,CAAC,KAAI;AAAA,SAEnF;AAAA,MACC,OAAO,aACP,oBAAC,UAAK,WAAU,oCAAoC,iBAAO,WAAU;AAAA,OAEvE;AAAA,KACD;AAEF;AAQO,MAAM,cAAc,WAA6C,SAASA,aAChF,EAAE,SAAS,cAAc,GAAG,UAAU,YAAY,aAAa,GAC/D,KACC;AACD,QAAM,MAAM,eAAe;AAC3B,MAAI,QAAQ,WAAW,GAAG;AACzB,WACC,oBAAC,SAAI,KAAU,WAAU,sDACvB,wBAAc,IAAI,6BAA6B,GACjD;AAAA,EAEF;AACA,SACC,oBAAC,SAAI,KAAU,WAAU,yBAAwB,MAAK,WACpD,kBAAQ,IAAI,CAAC,QAAQ,MAAM;AAC3B,UAAM,SAAS,MAAM;AACrB,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WACC,SACG,oEACA;AAAA,QAKJ,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM;AACd,cAAI,SAAU,UAAS,MAAM;AAAA,QAC9B;AAAA,QAEC,yBAAe,aAAa,MAAM,IAAI,oBAAC,oBAAiB,QAAgB;AAAA;AAAA,MAjBpE,OAAO;AAAA,IAkBb;AAAA,EAEF,CAAC,GACF;AAEF,CAAC;",
4
+ "sourcesContent": ["import { ReactNode, forwardRef } from 'react'\nimport { useTranslation } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { CommentAuthor } from './comment-author'\n\n/** A person the composer's \\@-mention picker can offer: a {@link CommentAuthor} plus its id and\n * picker-only display fields. @public */\nexport interface MentionMember extends CommentAuthor {\n\tid: string\n\t/** A secondary line under the name \u2014 e.g. an email or handle. Omit for a single-line row. */\n\tsecondary?: string\n\t/** Marks the current user; shown as a \"(You)\" suffix after the name. */\n\tyou?: boolean\n\t/** Hosts may carry extra fields; a custom `renderMember` can read them. */\n\t[key: string]: unknown\n}\n\n/** @public */\nexport interface MentionListProps {\n\t/** Members to choose from \u2014 already resolved for the current query by the host. */\n\tmembers: MentionMember[]\n\t/** Index of the highlighted row, driven by the composer's keyboard navigation. */\n\tactiveIndex?: number\n\t/** Called when a member is chosen (click, or Enter on the active row). */\n\tonSelect?(member: MentionMember): void\n\t/** Shown when no member matches the query. Defaults to a translated \"No matches\". */\n\temptyLabel?: string\n\t/** Override a row's content (inside the selectable button). Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The default row content: avatar, name (with a \"(You)\" marker), and an optional secondary line. */\nfunction DefaultMemberRow({ member }: { member: MentionMember }) {\n\tconst msg = useTranslation()\n\treturn (\n\t\t<>\n\t\t\t<Avatar author={member} />\n\t\t\t<span className=\"tlui-cmt-mention-list__text\">\n\t\t\t\t<span className=\"tlui-cmt-mention-list__name\">\n\t\t\t\t\t{member.name}\n\t\t\t\t\t{member.you && (\n\t\t\t\t\t\t<span className=\"tlui-cmt-mention-list__you\">{`(${msg('comments.mention-you')})`}</span>\n\t\t\t\t\t)}\n\t\t\t\t</span>\n\t\t\t\t{member.secondary && (\n\t\t\t\t\t<span className=\"tlui-cmt-mention-list__secondary\">{member.secondary}</span>\n\t\t\t\t)}\n\t\t\t</span>\n\t\t</>\n\t)\n}\n\n/**\n * The \\@-mention member picker: a popover list of people to mention, shown as the user types after\n * `@`. Presentational \u2014 the composer's suggestion plugin owns resolution, keyboard navigation, and\n * placement, and drives this with the host-resolved `members` and the highlighted `activeIndex`.\n * @public @react\n */\nexport const MentionList = forwardRef<HTMLDivElement, MentionListProps>(function MentionList(\n\t{ members, activeIndex = 0, onSelect, emptyLabel, renderMember },\n\tref\n) {\n\tconst msg = useTranslation()\n\tif (members.length === 0) {\n\t\treturn (\n\t\t\t<div ref={ref} className=\"tlui-cmt-mention-list tlui-cmt-mention-list--empty\">\n\t\t\t\t{emptyLabel ?? msg('comments.mention-no-matches')}\n\t\t\t</div>\n\t\t)\n\t}\n\treturn (\n\t\t<div ref={ref} className=\"tlui-cmt-mention-list\" role=\"listbox\">\n\t\t\t{members.map((member, i) => {\n\t\t\t\tconst active = i === activeIndex\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={member.id}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\taria-selected={active}\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-mention-list__item tlui-cmt-mention-list__item--active'\n\t\t\t\t\t\t\t\t: 'tlui-cmt-mention-list__item'\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Keep the composer focused on a row press: it's portaled outside the editor, so a\n\t\t\t\t\t\t// bare mousedown would blur the composer \u2014 which now dismisses the picker \u2014 before\n\t\t\t\t\t\t// the click could select. Selection runs on click; focus stays put.\n\t\t\t\t\t\tonMouseDown={(e) => e.preventDefault()}\n\t\t\t\t\t\tonClick={() => onSelect?.(member)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{renderMember ? renderMember(member) : <DefaultMemberRow member={member} />}\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": "AAmCE,mBACC,KAEC,YAHF;AAnCF,SAAoB,kBAAkB;AACtC,SAAS,sBAAsB;AAC/B,SAAS,cAAc;AA8BvB,SAAS,iBAAiB,EAAE,OAAO,GAA8B;AAChE,QAAM,MAAM,eAAe;AAC3B,SACC,iCACC;AAAA,wBAAC,UAAO,QAAQ,QAAQ;AAAA,IACxB,qBAAC,UAAK,WAAU,+BACf;AAAA,2BAAC,UAAK,WAAU,+BACd;AAAA,eAAO;AAAA,QACP,OAAO,OACP,oBAAC,UAAK,WAAU,8BAA8B,cAAI,IAAI,sBAAsB,CAAC,KAAI;AAAA,SAEnF;AAAA,MACC,OAAO,aACP,oBAAC,UAAK,WAAU,oCAAoC,iBAAO,WAAU;AAAA,OAEvE;AAAA,KACD;AAEF;AAQO,MAAM,cAAc,WAA6C,SAASA,aAChF,EAAE,SAAS,cAAc,GAAG,UAAU,YAAY,aAAa,GAC/D,KACC;AACD,QAAM,MAAM,eAAe;AAC3B,MAAI,QAAQ,WAAW,GAAG;AACzB,WACC,oBAAC,SAAI,KAAU,WAAU,sDACvB,wBAAc,IAAI,6BAA6B,GACjD;AAAA,EAEF;AACA,SACC,oBAAC,SAAI,KAAU,WAAU,yBAAwB,MAAK,WACpD,kBAAQ,IAAI,CAAC,QAAQ,MAAM;AAC3B,UAAM,SAAS,MAAM;AACrB,WACC;AAAA,MAAC;AAAA;AAAA,QAEA,MAAK;AAAA,QACL,MAAK;AAAA,QACL,iBAAe;AAAA,QACf,WACC,SACG,oEACA;AAAA,QAKJ,aAAa,CAAC,MAAM,EAAE,eAAe;AAAA,QACrC,SAAS,MAAM,WAAW,MAAM;AAAA,QAE/B,yBAAe,aAAa,MAAM,IAAI,oBAAC,oBAAiB,QAAgB;AAAA;AAAA,MAfpE,OAAO;AAAA,IAgBb;AAAA,EAEF,CAAC,GACF;AAEF,CAAC;",
6
6
  "names": ["MentionList"]
7
7
  }
@@ -104,14 +104,15 @@ function createMentionSuggestion(getSuggestions, options = {}) {
104
104
  if (container) container.style.display = "none";
105
105
  mentionPickerOpen.set(false);
106
106
  };
107
+ const popupProps = (props) => ({
108
+ items: props.items,
109
+ command: props.command,
110
+ renderMember: options.renderMember
111
+ });
107
112
  return {
108
113
  onStart: (props) => {
109
114
  renderer = new ReactRenderer(MentionPopup, {
110
- props: {
111
- items: props.items,
112
- command: props.command,
113
- renderMember: options.renderMember
114
- },
115
+ props: popupProps(props),
115
116
  editor: props.editor
116
117
  });
117
118
  editorEl = props.editor.view.dom;
@@ -125,12 +126,7 @@ function createMentionSuggestion(getSuggestions, options = {}) {
125
126
  mentionPickerOpen.set(true);
126
127
  },
127
128
  onUpdate: (props) => {
128
- if (renderer)
129
- renderer.updateProps({
130
- items: props.items,
131
- command: props.command,
132
- renderMember: options.renderMember
133
- });
129
+ renderer?.updateProps(popupProps(props));
134
130
  if (container) container.style.display = "";
135
131
  mentionPickerOpen.set(true);
136
132
  place();
@@ -150,14 +146,13 @@ function createMentionSuggestion(getSuggestions, options = {}) {
150
146
  }
151
147
  return true;
152
148
  }
153
- if (renderer && renderer.ref) return renderer.ref.onKeyDown(props);
154
- return false;
149
+ return renderer?.ref?.onKeyDown(props) ?? false;
155
150
  },
156
151
  onExit: () => {
157
152
  stopFollowing();
158
- if (editorEl) editorEl.removeEventListener("blur", hide);
159
- if (container) container.remove();
160
- if (renderer) renderer.destroy();
153
+ editorEl?.removeEventListener("blur", hide);
154
+ container?.remove();
155
+ renderer?.destroy();
161
156
  renderer = null;
162
157
  container = null;
163
158
  mentionPickerOpen.set(false);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/mention-suggestion.tsx"],
4
- "sourcesContent": ["import type { MentionNodeAttrs } from '@tiptap/extension-mention'\nimport { ReactRenderer } from '@tiptap/react'\nimport type { SuggestionKeyDownProps, SuggestionOptions } from '@tiptap/suggestion'\nimport { type ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { type Editor as TldrawEditor, atom, react, usePassThroughWheelEvents } from 'tldraw'\nimport { MentionList, MentionMember } from './mention-list'\n\n/** The handle the suggestion plugin drives \u2014 it forwards navigation keys into the popup. */\nexport interface MentionPopupHandle {\n\tonKeyDown(props: SuggestionKeyDownProps): boolean\n}\n\ninterface MentionPopupProps {\n\titems: MentionMember[]\n\tcommand(attrs: MentionNodeAttrs): void\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The live @-picker popup: owns the highlighted index and keyboard, renders the presentational list. */\nconst MentionPopup = forwardRef<MentionPopupHandle, MentionPopupProps>(function MentionPopup(\n\t{ items, command, renderMember },\n\tref\n) {\n\tconst [activeIndex, setActiveIndex] = useState(0)\n\t// A wheel over the popup drives the canvas beneath it, the same pass-through every tldraw panel gets;\n\t// the hook leaves the list alone while it scrolls its own overflow. The suggestion plugin builds the\n\t// popup imperatively, but `ReactRenderer` portals this into the composer's tree, so context reaches it.\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(listRef)\n\t// A new query yields new items; reset the highlight to the top during render \u2014 not in an effect,\n\t// which would leave a frame where `activeIndex` still points past a shrunk list and Enter selects\n\t// its (now out-of-range, undefined) item, swallowing the key without inserting a mention.\n\tconst [prevItems, setPrevItems] = useState(items)\n\tif (items !== prevItems) {\n\t\tsetPrevItems(items)\n\t\tsetActiveIndex(0)\n\t}\n\n\tconst select = (member: MentionMember | undefined) => {\n\t\tif (member) command({ id: member.id, label: member.name })\n\t}\n\n\tuseImperativeHandle(ref, () => ({\n\t\tonKeyDown: ({ event }) => {\n\t\t\tif (items.length === 0) return false\n\t\t\tif (event.key === 'ArrowUp') {\n\t\t\t\tsetActiveIndex((i) => (i + items.length - 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (event.key === 'ArrowDown') {\n\t\t\t\tsetActiveIndex((i) => (i + 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Enter and Tab both complete the highlighted member (falling back to the top match so a\n\t\t\t// stale index never selects `undefined`). The empty-roster case is handled a level up, in\n\t\t\t// the suggestion's onKeyDown, which cancels the picker.\n\t\t\tif (event.key === 'Enter' || event.key === 'Tab') {\n\t\t\t\tselect(items[activeIndex] ?? items[0])\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}))\n\n\treturn (\n\t\t<MentionList\n\t\t\tref={listRef}\n\t\t\tmembers={items}\n\t\t\tactiveIndex={activeIndex}\n\t\t\tonSelect={select}\n\t\t\trenderMember={renderMember}\n\t\t/>\n\t)\n})\n\nconst MAX_SUGGESTIONS = 8\n\n/** Members whose name contains the query (case-insensitive), capped to the popup's length.\n * @public */\nexport function filterMentionMembers(members: MentionMember[], query: string): MentionMember[] {\n\tconst q = query.toLowerCase()\n\treturn members.filter((m) => m.name.toLowerCase().includes(q)).slice(0, MAX_SUGGESTIONS)\n}\n\n// Deliberately NOT tldraw's open-menu registry: registering there mounts MenuClickCapture, which\n// makes the canvas inert. The picker is an inline autocomplete, not a modal \u2014 the canvas must stay\n// pannable beneath it \u2014 so track \"open\" ourselves.\nconst mentionPickerOpen = atom('isMentionPickerOpen', false)\n\n/**\n * Whether the \\@-mention picker is currently showing. Host dismissal (Escape, outside-click) checks\n * this so it can defer to the picker instead of tearing down the composer or thread beneath it.\n * @public\n */\nexport function isMentionPickerOpen(): boolean {\n\treturn mentionPickerOpen.get()\n}\n\n/** @public */\nexport interface MentionSuggestionOptions {\n\t/** Override a member row's content in the picker. Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n\t/**\n\t * The tldraw editor the composer lives in. When provided, the popup re-anchors reactively as the\n\t * canvas camera moves (the composer rides it) instead of polling every frame. Omit off-canvas.\n\t */\n\teditor?: TldrawEditor | null\n}\n\n/**\n * Build the TipTap `suggestion` config for the \\@-picker. `getSuggestions(query)` is the host's\n * resolver \u2014 the SDK owns neither the roster nor the filtering. The plugin runs outside React, so\n * `render` mounts `MentionPopup` via a `ReactRenderer` and forwards navigation keys to it.\n * @public\n */\nexport function createMentionSuggestion(\n\tgetSuggestions: (query: string) => MentionMember[] | Promise<MentionMember[]>,\n\toptions: MentionSuggestionOptions = {}\n): Omit<SuggestionOptions<MentionMember, MentionNodeAttrs>, 'editor'> {\n\treturn {\n\t\tchar: '@',\n\t\titems: ({ query }) => getSuggestions(query),\n\t\trender: () => {\n\t\t\tlet renderer: ReactRenderer<MentionPopupHandle, MentionPopupProps> | null = null\n\t\t\tlet container: HTMLElement | null = null\n\t\t\tlet editorEl: HTMLElement | null = null\n\t\t\tlet stopCameraReaction: (() => void) | null = null\n\t\t\t// The composer field's top-left in page space, plus the popup's screen width. Captured on a\n\t\t\t// fresh read so camera moves can re-derive the popup's screen position from the page anchor\n\t\t\t// (see reposition) rather than the field's DOM rect.\n\t\t\tlet anchorPage: { x: number; y: number } | null = null\n\t\t\tlet popupWidth = 0\n\n\t\t\tconst applyScreen = (left: number, top: number, width: number) => {\n\t\t\t\tif (!container) return\n\t\t\t\tcontainer.style.left = `${left}px`\n\t\t\t\tcontainer.style.top = `${top + 4}px`\n\t\t\t\tcontainer.style.width = `${width}px`\n\t\t\t}\n\n\t\t\t// Fresh placement: read the field's real screen rect, position the popup flush under it (not\n\t\t\t// the caret, matching its width), and remember the field's page-space anchor for reposition.\n\t\t\tconst place = () => {\n\t\t\t\tif (!container || !editorEl || container.style.display === 'none') return\n\t\t\t\tconst field = editorEl.closest('.tlui-cmt-composer__field') ?? editorEl\n\t\t\t\tconst rect = field.getBoundingClientRect()\n\t\t\t\tpopupWidth = rect.width\n\t\t\t\tanchorPage = options.editor?.screenToPage({ x: rect.left, y: rect.bottom }) ?? null\n\t\t\t\tapplyScreen(rect.left, rect.bottom, rect.width)\n\t\t\t}\n\n\t\t\t// Re-derived from the remembered page anchor \u2014 pure camera math, always current. Reading the field's\n\t\t\t// DOM rect would lag a frame: the composer re-positions on a React commit, after the camera reaction.\n\t\t\tconst reposition = () => {\n\t\t\t\tif (!anchorPage || !options.editor) return\n\t\t\t\tconst s = options.editor.pageToScreen(anchorPage)\n\t\t\t\tapplyScreen(s.x, s.y, popupWidth)\n\t\t\t}\n\n\t\t\t// The popup is `position: fixed`, but a canvas composer rides the camera, which moves it with no\n\t\t\t// scroll/resize event to hook. Re-anchor from a tldraw reaction rather than polling every frame,\n\t\t\t// plus on window scroll/resize for the off-canvas case.\n\t\t\tconst startFollowing = () => {\n\t\t\t\twindow.addEventListener('scroll', place, true)\n\t\t\t\twindow.addEventListener('resize', place)\n\t\t\t\tif (options.editor) {\n\t\t\t\t\tstopCameraReaction = react('anchor mention popup to camera', () => {\n\t\t\t\t\t\toptions.editor!.getCamera() // track the camera so this re-runs as it moves\n\t\t\t\t\t\treposition()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst stopFollowing = () => {\n\t\t\t\twindow.removeEventListener('scroll', place, true)\n\t\t\t\twindow.removeEventListener('resize', place)\n\t\t\t\tstopCameraReaction?.()\n\t\t\t\tstopCameraReaction = null\n\t\t\t\tanchorPage = null\n\t\t\t}\n\n\t\t\t// Dismiss the roster on Escape or blur, clearing the open flag so isMentionPickerOpen() stays\n\t\t\t// accurate and the thread's own dismissal can take over. Typing re-shows it via onUpdate.\n\t\t\tconst hide = () => {\n\t\t\t\tif (container) container.style.display = 'none'\n\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tonStart: (props) => {\n\t\t\t\t\trenderer = new ReactRenderer(MentionPopup, {\n\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\titems: props.items,\n\t\t\t\t\t\t\tcommand: props.command,\n\t\t\t\t\t\t\trenderMember: options.renderMember,\n\t\t\t\t\t\t},\n\t\t\t\t\t\teditor: props.editor,\n\t\t\t\t\t})\n\t\t\t\t\teditorEl = props.editor.view.dom as HTMLElement\n\t\t\t\t\t// The TipTap suggestion has no blur handling, so without this the picker would stay\n\t\t\t\t\t// \"open\" (and the thread would defer its Escape to it) after focus moved away from the\n\t\t\t\t\t// composer \u2014 leaving Escape a no-op and the roster stuck on screen.\n\t\t\t\t\teditorEl.addEventListener('blur', hide)\n\t\t\t\t\tcontainer = document.createElement('div')\n\t\t\t\t\tcontainer.className = 'tlui-cmt-mention-popup'\n\t\t\t\t\tcontainer.appendChild(renderer.element)\n\t\t\t\t\t// Mounted inside the tldraw container so the popup inherits the theme variables and the pass-through\n\t\t\t\t\t// hooks can find the canvas. Outside one, fall back to the body \u2014 the picker still works.\n\t\t\t\t\t;(editorEl.closest('.tl-container') ?? document.body).appendChild(container)\n\t\t\t\t\tplace()\n\t\t\t\t\tstartFollowing()\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t},\n\t\t\t\tonUpdate: (props) => {\n\t\t\t\t\tif (renderer)\n\t\t\t\t\t\trenderer.updateProps({\n\t\t\t\t\t\t\titems: props.items,\n\t\t\t\t\t\t\tcommand: props.command,\n\t\t\t\t\t\t\trenderMember: options.renderMember,\n\t\t\t\t\t\t})\n\t\t\t\t\t// Typing after an Escape re-shows the roster.\n\t\t\t\t\tif (container) container.style.display = ''\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t\tplace()\n\t\t\t\t},\n\t\t\t\tonKeyDown: (props) => {\n\t\t\t\t\t// Once the roster is hidden, the suggestion stays active but this handler goes inert \u2014 keys pass\n\t\t\t\t\t// through so a second Escape closes the composer. Typing re-shows the roster via onUpdate.\n\t\t\t\t\tif (!isMentionPickerOpen()) return false\n\t\t\t\t\tif (props.event.key === 'Escape') {\n\t\t\t\t\t\t// Dismiss only the roster: hide it and stop the key so the composer/thread beneath\n\t\t\t\t\t\t// doesn't also treat Escape as \"abandon\".\n\t\t\t\t\t\thide()\n\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (props.event.key === 'Enter' || props.event.key === 'Tab') {\n\t\t\t\t\t\t// Complete the highlighted member if there is one to complete; if the roster is empty\n\t\t\t\t\t\t// there's nothing to pick, so cancel the picker and swallow the key \u2014 the composer\n\t\t\t\t\t\t// beneath neither submits (Enter) nor moves focus / indents (Tab).\n\t\t\t\t\t\tconst completed = renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t\t\tif (!completed) {\n\t\t\t\t\t\t\thide()\n\t\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (renderer && renderer.ref) return renderer.ref.onKeyDown(props)\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t\tonExit: () => {\n\t\t\t\t\tstopFollowing()\n\t\t\t\t\tif (editorEl) editorEl.removeEventListener('blur', hide)\n\t\t\t\t\tif (container) container.remove()\n\t\t\t\t\tif (renderer) renderer.destroy()\n\t\t\t\t\trenderer = null\n\t\t\t\t\tcontainer = null\n\t\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MentionNodeAttrs } from '@tiptap/extension-mention'\nimport { ReactRenderer } from '@tiptap/react'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport { type ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { type Editor as TldrawEditor, atom, react, usePassThroughWheelEvents } from 'tldraw'\nimport { MentionList, MentionMember } from './mention-list'\n\n/** The handle the suggestion plugin drives \u2014 it forwards navigation keys into the popup. */\ninterface MentionPopupHandle {\n\tonKeyDown(props: SuggestionKeyDownProps): boolean\n}\n\ninterface MentionPopupProps {\n\titems: MentionMember[]\n\tcommand(attrs: MentionNodeAttrs): void\n\trenderMember?(member: MentionMember): ReactNode\n}\n\n/** The live @-picker popup: owns the highlighted index and keyboard, renders the presentational list. */\nconst MentionPopup = forwardRef<MentionPopupHandle, MentionPopupProps>(function MentionPopup(\n\t{ items, command, renderMember },\n\tref\n) {\n\tconst [activeIndex, setActiveIndex] = useState(0)\n\t// A wheel over the popup drives the canvas beneath it, the same pass-through every tldraw panel gets;\n\t// the hook leaves the list alone while it scrolls its own overflow. The suggestion plugin builds the\n\t// popup imperatively, but `ReactRenderer` portals this into the composer's tree, so context reaches it.\n\tconst listRef = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(listRef)\n\t// A new query yields new items; reset the highlight to the top during render \u2014 not in an effect,\n\t// which would leave a frame where `activeIndex` still points past a shrunk list and Enter selects\n\t// its (now out-of-range, undefined) item, swallowing the key without inserting a mention.\n\tconst [prevItems, setPrevItems] = useState(items)\n\tif (items !== prevItems) {\n\t\tsetPrevItems(items)\n\t\tsetActiveIndex(0)\n\t}\n\n\tconst select = (member: MentionMember | undefined) => {\n\t\tif (member) command({ id: member.id, label: member.name })\n\t}\n\n\tuseImperativeHandle(ref, () => ({\n\t\tonKeyDown: ({ event }) => {\n\t\t\tif (items.length === 0) return false\n\t\t\tif (event.key === 'ArrowUp') {\n\t\t\t\tsetActiveIndex((i) => (i + items.length - 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (event.key === 'ArrowDown') {\n\t\t\t\tsetActiveIndex((i) => (i + 1) % items.length)\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// Enter and Tab both complete the highlighted member (falling back to the top match so a\n\t\t\t// stale index never selects `undefined`). The empty-roster case is handled a level up, in\n\t\t\t// the suggestion's onKeyDown, which cancels the picker.\n\t\t\tif (event.key === 'Enter' || event.key === 'Tab') {\n\t\t\t\tselect(items[activeIndex] ?? items[0])\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\t}))\n\n\treturn (\n\t\t<MentionList\n\t\t\tref={listRef}\n\t\t\tmembers={items}\n\t\t\tactiveIndex={activeIndex}\n\t\t\tonSelect={select}\n\t\t\trenderMember={renderMember}\n\t\t/>\n\t)\n})\n\nconst MAX_SUGGESTIONS = 8\n\n/** Members whose name contains the query (case-insensitive), capped to the popup's length.\n * @public */\nexport function filterMentionMembers(members: MentionMember[], query: string): MentionMember[] {\n\tconst q = query.toLowerCase()\n\treturn members.filter((m) => m.name.toLowerCase().includes(q)).slice(0, MAX_SUGGESTIONS)\n}\n\n// Deliberately NOT tldraw's open-menu registry: registering there mounts MenuClickCapture, which\n// makes the canvas inert. The picker is an inline autocomplete, not a modal \u2014 the canvas must stay\n// pannable beneath it \u2014 so track \"open\" ourselves.\nconst mentionPickerOpen = atom('isMentionPickerOpen', false)\n\n/**\n * Whether the \\@-mention picker is currently showing. Host dismissal (Escape, outside-click) checks\n * this so it can defer to the picker instead of tearing down the composer or thread beneath it.\n * @public\n */\nexport function isMentionPickerOpen(): boolean {\n\treturn mentionPickerOpen.get()\n}\n\n/** @public */\nexport interface MentionSuggestionOptions {\n\t/** Override a member row's content in the picker. Defaults to avatar + name (+ secondary). */\n\trenderMember?(member: MentionMember): ReactNode\n\t/**\n\t * The tldraw editor the composer lives in. When provided, the popup re-anchors reactively as the\n\t * canvas camera moves (the composer rides it) instead of polling every frame. Omit off-canvas.\n\t */\n\teditor?: TldrawEditor | null\n}\n\n/**\n * Build the TipTap `suggestion` config for the \\@-picker. `getSuggestions(query)` is the host's\n * resolver \u2014 the SDK owns neither the roster nor the filtering. The plugin runs outside React, so\n * `render` mounts `MentionPopup` via a `ReactRenderer` and forwards navigation keys to it.\n * @public\n */\nexport function createMentionSuggestion(\n\tgetSuggestions: (query: string) => MentionMember[] | Promise<MentionMember[]>,\n\toptions: MentionSuggestionOptions = {}\n): Omit<SuggestionOptions<MentionMember, MentionNodeAttrs>, 'editor'> {\n\treturn {\n\t\tchar: '@',\n\t\titems: ({ query }) => getSuggestions(query),\n\t\trender: () => {\n\t\t\tlet renderer: ReactRenderer<MentionPopupHandle, MentionPopupProps> | null = null\n\t\t\tlet container: HTMLElement | null = null\n\t\t\tlet editorEl: HTMLElement | null = null\n\t\t\tlet stopCameraReaction: (() => void) | null = null\n\t\t\t// The composer field's top-left in page space, plus the popup's screen width. Captured on a\n\t\t\t// fresh read so camera moves can re-derive the popup's screen position from the page anchor\n\t\t\t// (see reposition) rather than the field's DOM rect.\n\t\t\tlet anchorPage: { x: number; y: number } | null = null\n\t\t\tlet popupWidth = 0\n\n\t\t\tconst applyScreen = (left: number, top: number, width: number) => {\n\t\t\t\tif (!container) return\n\t\t\t\tcontainer.style.left = `${left}px`\n\t\t\t\tcontainer.style.top = `${top + 4}px`\n\t\t\t\tcontainer.style.width = `${width}px`\n\t\t\t}\n\n\t\t\t// Fresh placement: read the field's real screen rect, position the popup flush under it (not\n\t\t\t// the caret, matching its width), and remember the field's page-space anchor for reposition.\n\t\t\tconst place = () => {\n\t\t\t\tif (!container || !editorEl || container.style.display === 'none') return\n\t\t\t\tconst field = editorEl.closest('.tlui-cmt-composer__field') ?? editorEl\n\t\t\t\tconst rect = field.getBoundingClientRect()\n\t\t\t\tpopupWidth = rect.width\n\t\t\t\tanchorPage = options.editor?.screenToPage({ x: rect.left, y: rect.bottom }) ?? null\n\t\t\t\tapplyScreen(rect.left, rect.bottom, rect.width)\n\t\t\t}\n\n\t\t\t// Re-derived from the remembered page anchor \u2014 pure camera math, always current. Reading the field's\n\t\t\t// DOM rect would lag a frame: the composer re-positions on a React commit, after the camera reaction.\n\t\t\tconst reposition = () => {\n\t\t\t\tif (!anchorPage || !options.editor) return\n\t\t\t\tconst s = options.editor.pageToScreen(anchorPage)\n\t\t\t\tapplyScreen(s.x, s.y, popupWidth)\n\t\t\t}\n\n\t\t\t// The popup is `position: fixed`, but a canvas composer rides the camera, which moves it with no\n\t\t\t// scroll/resize event to hook. Re-anchor from a tldraw reaction rather than polling every frame,\n\t\t\t// plus on window scroll/resize for the off-canvas case.\n\t\t\tconst startFollowing = () => {\n\t\t\t\twindow.addEventListener('scroll', place, true)\n\t\t\t\twindow.addEventListener('resize', place)\n\t\t\t\tif (options.editor) {\n\t\t\t\t\tstopCameraReaction = react('anchor mention popup to camera', () => {\n\t\t\t\t\t\toptions.editor!.getCamera() // track the camera so this re-runs as it moves\n\t\t\t\t\t\treposition()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst stopFollowing = () => {\n\t\t\t\twindow.removeEventListener('scroll', place, true)\n\t\t\t\twindow.removeEventListener('resize', place)\n\t\t\t\tstopCameraReaction?.()\n\t\t\t\tstopCameraReaction = null\n\t\t\t\tanchorPage = null\n\t\t\t}\n\n\t\t\t// Dismiss the roster on Escape or blur, clearing the open flag so isMentionPickerOpen() stays\n\t\t\t// accurate and the thread's own dismissal can take over. Typing re-shows it via onUpdate.\n\t\t\tconst hide = () => {\n\t\t\t\tif (container) container.style.display = 'none'\n\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t}\n\n\t\t\tconst popupProps = (\n\t\t\t\tprops: SuggestionProps<MentionMember, MentionNodeAttrs>\n\t\t\t): MentionPopupProps => ({\n\t\t\t\titems: props.items,\n\t\t\t\tcommand: props.command,\n\t\t\t\trenderMember: options.renderMember,\n\t\t\t})\n\n\t\t\treturn {\n\t\t\t\tonStart: (props) => {\n\t\t\t\t\trenderer = new ReactRenderer(MentionPopup, {\n\t\t\t\t\t\tprops: popupProps(props),\n\t\t\t\t\t\teditor: props.editor,\n\t\t\t\t\t})\n\t\t\t\t\teditorEl = props.editor.view.dom as HTMLElement\n\t\t\t\t\t// The TipTap suggestion has no blur handling, so without this the picker would stay\n\t\t\t\t\t// \"open\" (and the thread would defer its Escape to it) after focus moved away from the\n\t\t\t\t\t// composer \u2014 leaving Escape a no-op and the roster stuck on screen.\n\t\t\t\t\teditorEl.addEventListener('blur', hide)\n\t\t\t\t\tcontainer = document.createElement('div')\n\t\t\t\t\tcontainer.className = 'tlui-cmt-mention-popup'\n\t\t\t\t\tcontainer.appendChild(renderer.element)\n\t\t\t\t\t// Mounted inside the tldraw container so the popup inherits the theme variables and the pass-through\n\t\t\t\t\t// hooks can find the canvas. Outside one, fall back to the body \u2014 the picker still works.\n\t\t\t\t\t;(editorEl.closest('.tl-container') ?? document.body).appendChild(container)\n\t\t\t\t\tplace()\n\t\t\t\t\tstartFollowing()\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t},\n\t\t\t\tonUpdate: (props) => {\n\t\t\t\t\trenderer?.updateProps(popupProps(props))\n\t\t\t\t\t// Typing after an Escape re-shows the roster.\n\t\t\t\t\tif (container) container.style.display = ''\n\t\t\t\t\tmentionPickerOpen.set(true)\n\t\t\t\t\tplace()\n\t\t\t\t},\n\t\t\t\tonKeyDown: (props) => {\n\t\t\t\t\t// Once the roster is hidden, the suggestion stays active but this handler goes inert \u2014 keys pass\n\t\t\t\t\t// through so a second Escape closes the composer. Typing re-shows the roster via onUpdate.\n\t\t\t\t\tif (!isMentionPickerOpen()) return false\n\t\t\t\t\tif (props.event.key === 'Escape') {\n\t\t\t\t\t\t// Dismiss only the roster: hide it and stop the key so the composer/thread beneath\n\t\t\t\t\t\t// doesn't also treat Escape as \"abandon\".\n\t\t\t\t\t\thide()\n\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\tif (props.event.key === 'Enter' || props.event.key === 'Tab') {\n\t\t\t\t\t\t// Complete the highlighted member if there is one to complete; if the roster is empty\n\t\t\t\t\t\t// there's nothing to pick, so cancel the picker and swallow the key \u2014 the composer\n\t\t\t\t\t\t// beneath neither submits (Enter) nor moves focus / indents (Tab).\n\t\t\t\t\t\tconst completed = renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t\t\tif (!completed) {\n\t\t\t\t\t\t\thide()\n\t\t\t\t\t\t\tprops.event.stopPropagation()\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\treturn renderer?.ref?.onKeyDown(props) ?? false\n\t\t\t\t},\n\t\t\t\tonExit: () => {\n\t\t\t\t\tstopFollowing()\n\t\t\t\t\teditorEl?.removeEventListener('blur', hide)\n\t\t\t\t\tcontainer?.remove()\n\t\t\t\t\trenderer?.destroy()\n\t\t\t\t\trenderer = null\n\t\t\t\t\tcontainer = null\n\t\t\t\t\tmentionPickerOpen.set(false)\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["MentionPopup"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/mentions",
3
3
  "description": "tldraw @-mention picker and TipTap node, shared by commenting and shape rich text.",
4
- "version": "5.3.2",
4
+ "version": "5.4.0-canary.02cd0bd3b597",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -62,8 +62,8 @@
62
62
  "@tiptap/pm": "^3.12.1",
63
63
  "@tiptap/react": "^3.12.1",
64
64
  "@tiptap/suggestion": "3.13.0",
65
- "@tldraw/utils": "5.3.2",
66
- "tldraw": "5.3.2"
65
+ "@tldraw/utils": "5.4.0-canary.02cd0bd3b597",
66
+ "tldraw": "5.4.0-canary.02cd0bd3b597"
67
67
  },
68
68
  "module": "dist-esm/index.mjs",
69
69
  "source": "src/index.ts",
@@ -87,9 +87,7 @@ export const MentionList = forwardRef<HTMLDivElement, MentionListProps>(function
87
87
  // bare mousedown would blur the composer — which now dismisses the picker — before
88
88
  // the click could select. Selection runs on click; focus stays put.
89
89
  onMouseDown={(e) => e.preventDefault()}
90
- onClick={() => {
91
- if (onSelect) onSelect(member)
92
- }}
90
+ onClick={() => onSelect?.(member)}
93
91
  >
94
92
  {renderMember ? renderMember(member) : <DefaultMemberRow member={member} />}
95
93
  </button>
@@ -1,12 +1,12 @@
1
1
  import type { MentionNodeAttrs } from '@tiptap/extension-mention'
2
2
  import { ReactRenderer } from '@tiptap/react'
3
- import type { SuggestionKeyDownProps, SuggestionOptions } from '@tiptap/suggestion'
3
+ import type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'
4
4
  import { type ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'
5
5
  import { type Editor as TldrawEditor, atom, react, usePassThroughWheelEvents } from 'tldraw'
6
6
  import { MentionList, MentionMember } from './mention-list'
7
7
 
8
8
  /** The handle the suggestion plugin drives — it forwards navigation keys into the popup. */
9
- export interface MentionPopupHandle {
9
+ interface MentionPopupHandle {
10
10
  onKeyDown(props: SuggestionKeyDownProps): boolean
11
11
  }
12
12
 
@@ -185,14 +185,18 @@ export function createMentionSuggestion(
185
185
  mentionPickerOpen.set(false)
186
186
  }
187
187
 
188
+ const popupProps = (
189
+ props: SuggestionProps<MentionMember, MentionNodeAttrs>
190
+ ): MentionPopupProps => ({
191
+ items: props.items,
192
+ command: props.command,
193
+ renderMember: options.renderMember,
194
+ })
195
+
188
196
  return {
189
197
  onStart: (props) => {
190
198
  renderer = new ReactRenderer(MentionPopup, {
191
- props: {
192
- items: props.items,
193
- command: props.command,
194
- renderMember: options.renderMember,
195
- },
199
+ props: popupProps(props),
196
200
  editor: props.editor,
197
201
  })
198
202
  editorEl = props.editor.view.dom as HTMLElement
@@ -211,12 +215,7 @@ export function createMentionSuggestion(
211
215
  mentionPickerOpen.set(true)
212
216
  },
213
217
  onUpdate: (props) => {
214
- if (renderer)
215
- renderer.updateProps({
216
- items: props.items,
217
- command: props.command,
218
- renderMember: options.renderMember,
219
- })
218
+ renderer?.updateProps(popupProps(props))
220
219
  // Typing after an Escape re-shows the roster.
221
220
  if (container) container.style.display = ''
222
221
  mentionPickerOpen.set(true)
@@ -244,14 +243,13 @@ export function createMentionSuggestion(
244
243
  }
245
244
  return true
246
245
  }
247
- if (renderer && renderer.ref) return renderer.ref.onKeyDown(props)
248
- return false
246
+ return renderer?.ref?.onKeyDown(props) ?? false
249
247
  },
250
248
  onExit: () => {
251
249
  stopFollowing()
252
- if (editorEl) editorEl.removeEventListener('blur', hide)
253
- if (container) container.remove()
254
- if (renderer) renderer.destroy()
250
+ editorEl?.removeEventListener('blur', hide)
251
+ container?.remove()
252
+ renderer?.destroy()
255
253
  renderer = null
256
254
  container = null
257
255
  mentionPickerOpen.set(false)