@tightknitai/block-kitchen 0.10.17 → 0.10.19

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/README.md CHANGED
@@ -139,6 +139,7 @@ Given no height to work with — a plain `<div>` in ordinary document flow — i
139
139
  | `confirmUpdateLabel` | `string` | no | Label for the update dialog's final confirm button. Defaults to `'Update message'` (shows `'Updating…'` while in flight). |
140
140
  | `previewHooks` | `PreviewHooks` | no | Hooks forwarded to `slack-blocks-to-jsx`'s `<Message>` for resolving user / channel / emoji directives. |
141
141
  | `customEmojis` | `CustomEmoji[]` | no | Workspace custom emoji (`{ name, url, alias }`) the preview resolves. Entries with a `url` render `:name:` as the workspace image; alias entries (`url: null`) fall back to their target emoji. Render-only — never serialized into the emitted Block Kit JSON. A caller-supplied `previewHooks.emoji` takes precedence. |
142
+ | `mentions` | `{ searchUsers, getUserName? }` | no | Opt-in `@` mentions in the rich-text editor: typing `@` or the toolbar's Mention button opens a picker backed by `searchUsers(query, { signal })`, and a pick inserts a rich text `user` element. `getUserName(userId)` labels mentions in the editor. See [Mentions](#mentions-opt-in). |
142
143
  | `palette` | `PaletteSection[]` | no | The left-hand palette of draggable variants. Defaults to `defaultPalette`. Spread it to filter, reorder, or add your own pre-configured variants — see [Customizing the palette](#customizing-the-palette). |
143
144
  | `disabledBlockTypes` | `SupportedBlockType[]` | no | Block types to hide from the palette without rebuilding it. Filters at the variant level — a section keeps any variants whose block types aren't disabled; sections that end up empty are dropped. Convenient when you want the default palette minus a few types (e.g. `['image', 'table']` for a text-only builder). |
144
145
  | `paletteMode` | `'advanced' \| 'simple'` | no | How much of the palette the user meets first. `'advanced'` (default) renders the full sectioned palette with its search input. `'simple'` opens on a flat list of the variants flagged `basic` — in the built-in palette: **Header**, **Divider**, **Rich Text Section** and **Image** — with no search and an "Advanced" link at the top that swaps in the full palette. Nothing is removed; it's a smaller first screen. See [Simple vs advanced palette](#simple-vs-advanced-palette). |
@@ -607,6 +608,8 @@ import type {
607
608
  BlockKitchenSendProps, // the send trio + `onUpdate` + `renderSendExtras`
608
609
  LoadingConfig, // load an existing message (works in both modes)
609
610
  LoadedMessage, // the loaded target: channel + ts + verdict
611
+ MentionsConfig, // opt-in `@` mentions: host search + name lookup
612
+ MentionUser, // one person the `@` picker offers
610
613
  BlockKitchenComposeOnlyProps, // the trio explicitly absent + `primaryAction`
611
614
  PaletteSection,
612
615
  PaletteVariant,
@@ -635,6 +638,30 @@ A searchable emoji picker (search, categories, skin tone, recents) is built into
635
638
 
636
639
  Pass [`customEmojis`](#props) to add your workspace's custom emoji: image entries appear as the picker's **Custom** category and render in the preview; alias entries fall back to their target. Custom emoji never block Send — Slack accepts unknown emoji names, so they're excluded from validation. The emitted Block Kit JSON is unchanged whether or not `customEmojis` is passed.
637
640
 
641
+ ## Mentions (opt-in)
642
+
643
+ Pass `mentions` to let people `@`-mention someone in the rich-text WYSIWYG editor. Typing `@` (after a space or at the start of a line, so `name@domain` is left alone) or clicking the toolbar's **Mention** button opens a picker backed by your search. Picking someone inserts a rich text `user` element, `{ type: "user", user_id }`. The picker's name and avatar are never serialized.
644
+
645
+ ```tsx
646
+ <BlockKitchen
647
+ mentions={{
648
+ // Called with the text after `@` (empty right after it); debounced, and
649
+ // `signal` aborts once a newer query supersedes this one.
650
+ searchUsers: async (query, { signal }) => {
651
+ const res = await fetch(`/api/members?q=${encodeURIComponent(query)}`, { signal });
652
+ const members = await res.json();
653
+ return members.map((m) => ({ id: m.slackUserId, name: m.name, avatarUrl: m.avatar }));
654
+ },
655
+ // Optional: labels mentions in the editor by id, including ones that came
656
+ // with the draft or a loaded message. Return undefined when unknown.
657
+ getUserName: (userId) => names.get(userId)
658
+ }}
659
+ {...rest}
660
+ />
661
+ ```
662
+
663
+ Mentions already in a draft round-trip through the WYSIWYG with or without `mentions`; without `getUserName` they show the picked name or the raw id. The builder never looks people up itself. To show names in the Slack preview too, resolve the ids in [`previewHooks.user`](#props).
664
+
638
665
  ## Validation
639
666
 
640
667
  Defense-in-depth: blocks are validated against [slack-block-kit-validator](https://github.com/TightknitAI/slack-block-kit-validator) before send. Issues are surfaced in the issues sheet with line numbers — users can fix them inline before posting.
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-Cxh1yNzW.cjs';
2
- export { a1 as SupportedBlockType } from './palette-Cxh1yNzW.cjs';
1
+ import { S as SupportedBlock } from './palette-DNUiqoQK.cjs';
2
+ export { a3 as SupportedBlockType } from './palette-DNUiqoQK.cjs';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SupportedBlock } from './palette-Cxh1yNzW.js';
2
- export { a1 as SupportedBlockType } from './palette-Cxh1yNzW.js';
1
+ import { S as SupportedBlock } from './palette-DNUiqoQK.js';
2
+ export { a3 as SupportedBlockType } from './palette-DNUiqoQK.js';
3
3
  import 'react';
4
4
  import 'slack-web-api-client';
5
5
 
package/dist/index.cjs CHANGED
@@ -22,6 +22,7 @@ var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
22
22
  var Link = require('@tiptap/extension-link');
23
23
  var react = require('@tiptap/react');
24
24
  var StarterKit = require('@tiptap/starter-kit');
25
+ var suggestion = require('@tiptap/suggestion');
25
26
 
26
27
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
27
28
 
@@ -1863,6 +1864,13 @@ function CustomEmojiProvider({ customEmojis, children }) {
1863
1864
  function useCustomEmojis() {
1864
1865
  return React.useContext(CustomEmojiContext);
1865
1866
  }
1867
+ var MentionsContext = React.createContext(null);
1868
+ function MentionsProvider({ mentions, children }) {
1869
+ return /* @__PURE__ */ jsxRuntime.jsx(MentionsContext.Provider, { value: mentions ?? null, children });
1870
+ }
1871
+ function useMentions() {
1872
+ return React.useContext(MentionsContext);
1873
+ }
1866
1874
 
1867
1875
  // src/lib/error-grouping.ts
1868
1876
  function toValidatorSurface(surface) {
@@ -3363,12 +3371,27 @@ function ChannelCombobox({
3363
3371
  ] });
3364
3372
  }
3365
3373
  var EMBED_SELECTOR = "iframe[src], embed[src], object[data], source[src], video[src], audio[src], track[src]";
3374
+ function useLatestHooks(hooks) {
3375
+ const latest = React.useRef(hooks);
3376
+ latest.current = hooks;
3377
+ const [live] = React.useState(
3378
+ () => new Proxy(
3379
+ {},
3380
+ {
3381
+ get: (_target, key) => typeof key === "string" ? latest.current?.[key] : void 0,
3382
+ has: (_target, key) => typeof key === "string" && latest.current !== void 0 && key in latest.current
3383
+ }
3384
+ )
3385
+ );
3386
+ return live;
3387
+ }
3366
3388
  function SlackBlockPreview({
3367
3389
  block,
3368
3390
  hooks,
3369
3391
  theme = "light"
3370
3392
  }) {
3371
3393
  const rootRef = React.useRef(null);
3394
+ const liveHooks = useLatestHooks(hooks);
3372
3395
  const safeBlock = React.useMemo(() => sanitizeBlock(block), [block]);
3373
3396
  React.useEffect(() => {
3374
3397
  const root = rootRef.current;
@@ -3423,7 +3446,7 @@ function SlackBlockPreview({
3423
3446
  withoutWrapper: true,
3424
3447
  theme,
3425
3448
  blocks: [safeBlock],
3426
- hooks
3449
+ hooks: liveHooks
3427
3450
  }
3428
3451
  ) })
3429
3452
  );
@@ -5196,14 +5219,16 @@ function makeEmojiImportResolver(customByName, index = getEmojiIndex()) {
5196
5219
  }
5197
5220
 
5198
5221
  // src/lib/rich-text-tiptap.ts
5222
+ var USER_MENTION_NODE = "userMention";
5199
5223
  var SUPPORTED_BLOCK_KINDS = /* @__PURE__ */ new Set([
5200
5224
  "rich_text_section",
5201
5225
  "rich_text_list",
5202
5226
  "rich_text_quote",
5203
5227
  "rich_text_preformatted"
5204
5228
  ]);
5205
- var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji"]);
5229
+ var SUPPORTED_INLINE_KINDS = /* @__PURE__ */ new Set(["text", "link", "emoji", "user"]);
5206
5230
  var SUPPORTED_TEXT_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline", "code"]);
5231
+ var SUPPORTED_USER_STYLE_KEYS = /* @__PURE__ */ new Set(["bold", "italic", "strike", "underline"]);
5207
5232
  var MAX_LIST_INDENT = 8;
5208
5233
  function detectLossy(block) {
5209
5234
  const out = [];
@@ -5226,6 +5251,17 @@ function detectLossy(block) {
5226
5251
  if ("border" in el && (el.border ?? 0) > 0) {
5227
5252
  out.push({ label: "Quote/preformatted border", where });
5228
5253
  }
5254
+ if (el.type === "rich_text_preformatted") {
5255
+ el.elements?.forEach((inline, i) => {
5256
+ const sub = `${where}.elements[${i}]`;
5257
+ if (inline.type !== "text") {
5258
+ out.push({ label: `In code block: ${inline.type}`, where: sub });
5259
+ } else if (Object.values(inline.style ?? {}).some(Boolean)) {
5260
+ out.push({ label: "Styled text in code block", where: sub });
5261
+ }
5262
+ });
5263
+ return;
5264
+ }
5229
5265
  scanInlines(el.elements ?? [], where, out);
5230
5266
  });
5231
5267
  return out;
@@ -5247,6 +5283,13 @@ function scanInlines(inlines, where, out) {
5247
5283
  }
5248
5284
  }
5249
5285
  }
5286
+ if (inline.type === "user") {
5287
+ for (const k of Object.keys(inline.style ?? {})) {
5288
+ if (!SUPPORTED_USER_STYLE_KEYS.has(k)) {
5289
+ out.push({ label: `Mention style: ${k}`, where: sub });
5290
+ }
5291
+ }
5292
+ }
5250
5293
  });
5251
5294
  }
5252
5295
  var defaultEmojiResolver = (el) => ({
@@ -5358,6 +5401,17 @@ function inlinesToPM(inlines, resolveEmoji) {
5358
5401
  continue;
5359
5402
  }
5360
5403
  out.push({ type: "emoji", attrs });
5404
+ } else if (inline.type === "user") {
5405
+ const user = inline;
5406
+ if (!user.user_id) {
5407
+ continue;
5408
+ }
5409
+ const node = { type: USER_MENTION_NODE, attrs: { userId: user.user_id, name: null } };
5410
+ const marks = styleToMarks(user.style);
5411
+ if (marks.length > 0) {
5412
+ node.marks = marks;
5413
+ }
5414
+ out.push(node);
5361
5415
  } else if (inline.type === "link") {
5362
5416
  const link = inline;
5363
5417
  const text = link.text || link.url || "";
@@ -5491,6 +5545,19 @@ function proseMirrorInlinesToRichTextElements(nodes) {
5491
5545
  out.push(emoji);
5492
5546
  continue;
5493
5547
  }
5548
+ if (node.type === USER_MENTION_NODE) {
5549
+ const userId = String(node.attrs?.userId ?? "");
5550
+ if (!userId) {
5551
+ continue;
5552
+ }
5553
+ const user = { type: "user", user_id: userId };
5554
+ const style2 = userStyle(node.marks ?? []);
5555
+ if (Object.keys(style2).length > 0) {
5556
+ user.style = style2;
5557
+ }
5558
+ out.push(user);
5559
+ continue;
5560
+ }
5494
5561
  if (node.type === "hardBreak") {
5495
5562
  out.push({ type: "text", text: "\n" });
5496
5563
  continue;
@@ -5545,6 +5612,15 @@ function marksToStyle(marks) {
5545
5612
  }
5546
5613
  return style;
5547
5614
  }
5615
+ function userStyle(marks) {
5616
+ const style = {};
5617
+ for (const [key, value] of Object.entries(marksToStyle(marks))) {
5618
+ if (SUPPORTED_USER_STYLE_KEYS.has(key)) {
5619
+ style[key] = value;
5620
+ }
5621
+ }
5622
+ return style;
5623
+ }
5548
5624
  function mergeAdjacentTextRuns(inlines) {
5549
5625
  const out = [];
5550
5626
  for (const inline of inlines) {
@@ -5611,6 +5687,91 @@ var EmojiNode = react.Node.create({
5611
5687
  return react.ReactNodeViewRenderer(EmojiNodeView);
5612
5688
  }
5613
5689
  });
5690
+ var MENTION_SEARCH_DEBOUNCE_MS = 200;
5691
+ function insertUserMention(editor, range, user) {
5692
+ const to = editor.state.doc.textBetween(range.to, Math.min(range.to + 1, editor.state.doc.content.size)) === " " ? range.to + 1 : range.to;
5693
+ editor.chain().focus().insertContentAt({ from: range.from, to }, [
5694
+ { type: USER_MENTION_NODE, attrs: { userId: user.id, name: user.name } },
5695
+ { type: "text", text: " " }
5696
+ ]).run();
5697
+ }
5698
+ function insertMentionTrigger(editor) {
5699
+ const { from } = editor.state.selection;
5700
+ const before = from > 1 ? editor.state.doc.textBetween(from - 1, from, void 0, "\uFFFC") : "";
5701
+ const needsSpace = before !== "" && !/\s/.test(before);
5702
+ editor.chain().focus().insertContent(needsSpace ? " @" : "@").run();
5703
+ }
5704
+ function dismissMentionSuggestion(editor) {
5705
+ suggestion.exitSuggestion(editor.view);
5706
+ }
5707
+ var MentionSuggestion = react.Extension.create({
5708
+ name: "userMentionSuggestion",
5709
+ addOptions() {
5710
+ return {
5711
+ isEnabled: () => false,
5712
+ search: async () => [],
5713
+ render: () => ({})
5714
+ };
5715
+ },
5716
+ addProseMirrorPlugins() {
5717
+ const { isEnabled, search, render } = this.options;
5718
+ return [
5719
+ suggestion.Suggestion({
5720
+ editor: this.editor,
5721
+ char: "@",
5722
+ debounce: MENTION_SEARCH_DEBOUNCE_MS,
5723
+ allow: () => isEnabled(),
5724
+ items: ({ query, signal }) => search(query, signal),
5725
+ command: ({ editor, range, props }) => insertUserMention(editor, range, props),
5726
+ render
5727
+ })
5728
+ ];
5729
+ }
5730
+ });
5731
+ function userMentionLabel(attrs, getUserName) {
5732
+ return getUserName?.(attrs.userId) ?? attrs.name ?? attrs.userId;
5733
+ }
5734
+ function UserMentionNodeView({ node }) {
5735
+ const mentions = useMentions();
5736
+ const attrs = node.attrs;
5737
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { as: "span", className: "bk-user-mention", "data-user-mention": attrs.userId, children: [
5738
+ "@",
5739
+ userMentionLabel(attrs, mentions?.getUserName)
5740
+ ] });
5741
+ }
5742
+ var UserMentionNode = react.Node.create({
5743
+ name: USER_MENTION_NODE,
5744
+ group: "inline",
5745
+ inline: true,
5746
+ atom: true,
5747
+ selectable: true,
5748
+ addAttributes() {
5749
+ return {
5750
+ userId: {
5751
+ default: "",
5752
+ parseHTML: (element) => element.getAttribute("data-user-mention") ?? "",
5753
+ renderHTML: (attributes) => ({ "data-user-mention": attributes.userId })
5754
+ },
5755
+ name: {
5756
+ default: null,
5757
+ parseHTML: (element) => element.getAttribute("data-user-name"),
5758
+ renderHTML: (attributes) => attributes.name ? { "data-user-name": attributes.name } : {}
5759
+ }
5760
+ };
5761
+ },
5762
+ parseHTML() {
5763
+ return [{ tag: "span[data-user-mention]" }];
5764
+ },
5765
+ renderHTML({ node, HTMLAttributes }) {
5766
+ return ["span", react.mergeAttributes(HTMLAttributes), `@${userMentionLabel(node.attrs)}`];
5767
+ },
5768
+ renderText({ node }) {
5769
+ return `@${userMentionLabel(node.attrs)}`;
5770
+ },
5771
+ addNodeView() {
5772
+ return react.ReactNodeViewRenderer(UserMentionNodeView);
5773
+ }
5774
+ });
5614
5775
 
5615
5776
  // src/lib/emoji-recents.ts
5616
5777
  var STORAGE_KEY = "bk:emoji-recents";
@@ -5892,6 +6053,133 @@ function EmojiPickerButton({
5892
6053
  ) })
5893
6054
  ] });
5894
6055
  }
6056
+ function nextMentionPickerState(props, prev) {
6057
+ return {
6058
+ items: props.loading && prev ? prev.items : props.items,
6059
+ loading: props.loading,
6060
+ clientRect: props.clientRect ?? null,
6061
+ command: props.command
6062
+ };
6063
+ }
6064
+ function MentionPicker({
6065
+ editor,
6066
+ state,
6067
+ keyHandlerRef,
6068
+ onDismiss
6069
+ }) {
6070
+ const { items, loading, clientRect, command } = state;
6071
+ const [activeIndex, setActiveIndex] = React.useState(0);
6072
+ const [shownItems, setShownItems] = React.useState(items);
6073
+ if (shownItems !== items) {
6074
+ setShownItems(items);
6075
+ setActiveIndex(0);
6076
+ }
6077
+ const listRef = React.useRef(null);
6078
+ React.useEffect(() => {
6079
+ keyHandlerRef.current = (event) => {
6080
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
6081
+ if (items.length > 0) {
6082
+ const step = event.key === "ArrowDown" ? 1 : -1;
6083
+ setActiveIndex((index) => (index + step + items.length) % items.length);
6084
+ }
6085
+ return true;
6086
+ }
6087
+ if (event.key === "Enter" || event.key === "Tab") {
6088
+ const user = items[activeIndex];
6089
+ if (!user) {
6090
+ return false;
6091
+ }
6092
+ command(user);
6093
+ return true;
6094
+ }
6095
+ return false;
6096
+ };
6097
+ return () => {
6098
+ keyHandlerRef.current = () => false;
6099
+ };
6100
+ }, [keyHandlerRef, items, activeIndex, command]);
6101
+ React.useEffect(() => {
6102
+ const row = listRef.current?.querySelectorAll('[role="option"]')[activeIndex];
6103
+ row?.scrollIntoView?.({ block: "nearest" });
6104
+ }, [activeIndex]);
6105
+ const anchorRef = React.useMemo(
6106
+ () => ({
6107
+ current: {
6108
+ getBoundingClientRect: () => clientRect?.() ?? editor.view.dom.getBoundingClientRect()
6109
+ }
6110
+ }),
6111
+ [clientRect, editor]
6112
+ );
6113
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6114
+ Popover,
6115
+ {
6116
+ open: true,
6117
+ onOpenChange: (open) => {
6118
+ if (!open) {
6119
+ onDismiss();
6120
+ }
6121
+ },
6122
+ children: [
6123
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverAnchor, { virtualRef: anchorRef }),
6124
+ /* @__PURE__ */ jsxRuntime.jsx(
6125
+ PopoverContent,
6126
+ {
6127
+ side: "bottom",
6128
+ align: "start",
6129
+ sideOffset: 4,
6130
+ className: "max-h-[min(15rem,var(--radix-popover-content-available-height))] w-64 p-1",
6131
+ onOpenAutoFocus: (e) => e.preventDefault(),
6132
+ onCloseAutoFocus: (e) => e.preventDefault(),
6133
+ onEscapeKeyDown: (e) => e.stopPropagation(),
6134
+ onInteractOutside: (e) => {
6135
+ if (e.target instanceof Node && editor.view.dom.contains(e.target)) {
6136
+ e.preventDefault();
6137
+ }
6138
+ },
6139
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: listRef, role: "listbox", "aria-label": "People to mention", children: items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 py-3 text-center text-xs text-muted-foreground", children: loading ? "Searching\u2026" : "No people match that search." }) : items.map((user, index) => (
6140
+ // biome-ignore lint/a11y/useFocusableInteractive: focus stays in the editor, which drives this list through the suggestion plugin's key handling.
6141
+ // biome-ignore lint/a11y/useKeyWithClickEvents: the keyboard path is the editor's, forwarded via keyHandlerRef.
6142
+ /* @__PURE__ */ jsxRuntime.jsxs(
6143
+ "div",
6144
+ {
6145
+ role: "option",
6146
+ "aria-selected": index === activeIndex,
6147
+ onPointerDown: (e) => e.preventDefault(),
6148
+ onClick: () => command(user),
6149
+ onMouseMove: () => setActiveIndex(index),
6150
+ className: cn(
6151
+ "flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground",
6152
+ index === activeIndex && "bg-accent"
6153
+ ),
6154
+ children: [
6155
+ /* @__PURE__ */ jsxRuntime.jsx(MentionAvatar, { user }),
6156
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: user.name })
6157
+ ]
6158
+ },
6159
+ user.id
6160
+ )
6161
+ )) })
6162
+ }
6163
+ )
6164
+ ]
6165
+ }
6166
+ );
6167
+ }
6168
+ function MentionAvatar({ user }) {
6169
+ const [failed, setFailed] = React.useState(false);
6170
+ const src = user.avatarUrl && isSafeImageSrc(user.avatarUrl) ? user.avatarUrl : null;
6171
+ if (src && !failed) {
6172
+ return /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: "", className: "h-5 w-5 shrink-0 rounded object-cover", onError: () => setFailed(true) });
6173
+ }
6174
+ return /* @__PURE__ */ jsxRuntime.jsx(
6175
+ "span",
6176
+ {
6177
+ "aria-hidden": "true",
6178
+ className: "flex h-5 w-5 shrink-0 items-center justify-center rounded bg-muted text-[10px] font-medium text-muted-foreground",
6179
+ children: Array.from(user.name.trim())[0]?.toUpperCase() ?? "@"
6180
+ }
6181
+ );
6182
+ }
5895
6183
  var BLOCK_ELEMENT_LABEL = {
5896
6184
  rich_text_section: "Section",
5897
6185
  rich_text_list: "List",
@@ -6467,7 +6755,7 @@ function LossyBanner({ reasons }) {
6467
6755
  ] });
6468
6756
  }
6469
6757
  var RICH_TEXT_INPUT_RULE_ALLOWLIST = ["bold", "italic", "strike", "code"];
6470
- function buildRichTextExtensions() {
6758
+ function buildRichTextExtensions(mentionSuggestion) {
6471
6759
  return [
6472
6760
  StarterKit__default.default.configure({
6473
6761
  heading: false,
@@ -6500,7 +6788,11 @@ function buildRichTextExtensions() {
6500
6788
  // via the link button.
6501
6789
  shouldAutoLink: hasExplicitSafeScheme,
6502
6790
  HTMLAttributes: { rel: "noreferrer noopener", target: "_blank" }
6503
- })
6791
+ }),
6792
+ // Always registered, so mentions already in the content round-trip with
6793
+ // or without the picker.
6794
+ UserMentionNode,
6795
+ ...mentionSuggestion ? [MentionSuggestion.configure(mentionSuggestion)] : []
6504
6796
  ];
6505
6797
  }
6506
6798
  function RichTextWysiwygEditor({ block, onChange }) {
@@ -6527,8 +6819,25 @@ function RichTextWysiwygEditor({ block, onChange }) {
6527
6819
  },
6528
6820
  [onChange]
6529
6821
  );
6822
+ const mentions = useMentions();
6823
+ const mentionsEnabled = mentions !== null;
6824
+ const mentionsRef = React.useRef(mentions);
6825
+ React.useEffect(() => {
6826
+ mentionsRef.current = mentions;
6827
+ }, [mentions]);
6828
+ const [mentionPicker, setMentionPicker] = React.useState(null);
6829
+ const mentionKeyHandlerRef = React.useRef(() => false);
6530
6830
  const editor = react.useEditor({
6531
- extensions: buildRichTextExtensions(),
6831
+ extensions: buildRichTextExtensions({
6832
+ isEnabled: () => mentionsRef.current !== null,
6833
+ search: async (query, signal) => await mentionsRef.current?.searchUsers(query, { signal }) ?? [],
6834
+ render: () => ({
6835
+ onStart: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6836
+ onUpdate: (props) => setMentionPicker((prev) => nextMentionPickerState(props, prev)),
6837
+ onExit: () => setMentionPicker(null),
6838
+ onKeyDown: ({ event }) => mentionKeyHandlerRef.current(event)
6839
+ })
6840
+ }),
6532
6841
  enableInputRules: RICH_TEXT_INPUT_RULE_ALLOWLIST,
6533
6842
  content: richTextToProseMirror(block, resolveEmoji),
6534
6843
  editorProps: {
@@ -6538,10 +6847,16 @@ function RichTextWysiwygEditor({ block, onChange }) {
6538
6847
  },
6539
6848
  onUpdate: ({ editor: ed }) => handleUpdate(ed)
6540
6849
  });
6850
+ const seenBlockRef = React.useRef(block);
6541
6851
  React.useEffect(() => {
6542
6852
  if (!editor) {
6543
6853
  return;
6544
6854
  }
6855
+ const blockChanged = seenBlockRef.current !== block;
6856
+ seenBlockRef.current = block;
6857
+ if (!blockChanged && lastEmittedRef.current) {
6858
+ return;
6859
+ }
6545
6860
  if (lastEmittedRef.current && JSON.stringify(lastEmittedRef.current) === JSON.stringify(block)) {
6546
6861
  return;
6547
6862
  }
@@ -6553,11 +6868,26 @@ function RichTextWysiwygEditor({ block, onChange }) {
6553
6868
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-[120px] rounded-md border border-input bg-background" });
6554
6869
  }
6555
6870
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2", children: [
6556
- /* @__PURE__ */ jsxRuntime.jsx(Toolbar, { editor }),
6557
- /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor })
6871
+ /* @__PURE__ */ jsxRuntime.jsx(
6872
+ Toolbar,
6873
+ {
6874
+ editor,
6875
+ mention: mentionsEnabled ? { active: mentionPicker !== null, insert: () => insertMentionTrigger(editor) } : null
6876
+ }
6877
+ ),
6878
+ /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor }),
6879
+ mentionPicker && /* @__PURE__ */ jsxRuntime.jsx(
6880
+ MentionPicker,
6881
+ {
6882
+ editor,
6883
+ state: mentionPicker,
6884
+ keyHandlerRef: mentionKeyHandlerRef,
6885
+ onDismiss: () => dismissMentionSuggestion(editor)
6886
+ }
6887
+ )
6558
6888
  ] });
6559
6889
  }
6560
- function Toolbar({ editor }) {
6890
+ function Toolbar({ editor, mention }) {
6561
6891
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-0.5 rounded-md border bg-muted/30 p-1", children: [
6562
6892
  /* @__PURE__ */ jsxRuntime.jsx(
6563
6893
  ToolbarButton,
@@ -6616,6 +6946,7 @@ function Toolbar({ editor }) {
6616
6946
  }).run()
6617
6947
  }
6618
6948
  ),
6949
+ mention && /* @__PURE__ */ jsxRuntime.jsx(ToolbarButton, { label: "Mention", active: mention.active, onClick: mention.insert, Icon: lucideReact.AtSign }),
6619
6950
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
6620
6951
  /* @__PURE__ */ jsxRuntime.jsx(
6621
6952
  ToolbarButton,
@@ -10952,6 +11283,7 @@ function BlockKitchen(props) {
10952
11283
  onValidationChange,
10953
11284
  previewHooks,
10954
11285
  customEmojis,
11286
+ mentions,
10955
11287
  loadChannels,
10956
11288
  loadSendAsUserStatus,
10957
11289
  onSend,
@@ -11312,7 +11644,7 @@ function BlockKitchen(props) {
11312
11644
  }
11313
11645
  )
11314
11646
  ] }),
11315
- /* @__PURE__ */ jsxRuntime.jsx(
11647
+ /* @__PURE__ */ jsxRuntime.jsx(MentionsProvider, { mentions, children: /* @__PURE__ */ jsxRuntime.jsx(
11316
11648
  Surface,
11317
11649
  {
11318
11650
  blocks,
@@ -11332,7 +11664,7 @@ function BlockKitchen(props) {
11332
11664
  isPaletteDrag: activePaletteVariant !== null,
11333
11665
  onOpenPalette: () => setPaletteOpen(true)
11334
11666
  }
11335
- )
11667
+ ) })
11336
11668
  ] })
11337
11669
  ]
11338
11670
  }